Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d11a674a3c | ||
|
|
31f70913a2 | ||
|
|
bd23faa28f | ||
|
|
5b1fbe1e2f | ||
|
|
96265b75de | ||
|
|
17298f0b2c | ||
|
|
61b475fa8d | ||
|
|
455084c17b | ||
|
|
c3c5b897f3 | ||
|
|
a1871a2982 | ||
|
|
87de017afd | ||
|
|
5a2c263af3 | ||
|
|
ddd2e704db | ||
|
|
9b5e0b0591 | ||
|
|
9b6636e21a | ||
|
|
a8358b8e7a | ||
|
|
5f091bb6ab | ||
|
|
17b334aaad | ||
|
|
c65209e6a9 | ||
|
|
d2518b11a7 | ||
|
|
70cdf3865a | ||
|
|
7c02ffb6c9 | ||
|
|
f2d9881382 | ||
|
|
d7761ab30e | ||
|
|
bf83c58982 | ||
|
|
2e082932c9 | ||
|
|
2f90cac52a | ||
|
|
20a0071c0d | ||
|
|
a40f83d6b6 | ||
|
|
0db8cc4206 | ||
|
|
97ddc027cb | ||
|
|
82c43ba2e3 | ||
|
|
799fb091b8 | ||
|
|
5ead1a087d | ||
|
|
64631a4552 | ||
|
|
f47cb04860 |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[*.{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
|
||||||
115
.eslintrc.js
Normal file
115
.eslintrc.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 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
|
||||||
59
.github/workflows/checks.build.yaml
vendored
Normal file
59
.github/workflows/checks.build.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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 }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Quality checks
|
name: quality-checks
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
@@ -8,12 +8,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
lint-command:
|
lint-command:
|
||||||
- npm run lint:vue
|
- npm run lint:eslint
|
||||||
- npm run lint:yaml
|
- npm run lint:yaml
|
||||||
- npm run lint:md
|
- npm run lint:md
|
||||||
- npm run lint:md:relative-urls
|
- npm run lint:md:relative-urls
|
||||||
- npm run lint:md:consistency
|
- npm run lint:md:consistency
|
||||||
fail-fast: false # So it continues with other commands if one fails
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Security checks
|
name: security-checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy desktop
|
name: release-desktop
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Bump & release
|
name: release-git
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push: # Ensure a new release is created for each new tag
|
push: # Ensure a new release is created for each new tag
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy site
|
name: release-site
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
28
.github/workflows/tests.e2e.yaml
vendored
Normal file
28
.github/workflows/tests.e2e.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
name: Test
|
name: integration-tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule: # for integration tests
|
schedule: # To get notified about problems from third party dependencies
|
||||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -25,9 +25,6 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
|
||||||
name: Run unit tests
|
|
||||||
run: npm run test:unit
|
|
||||||
-
|
-
|
||||||
name: Run integration tests
|
name: Run integration tests
|
||||||
run: npm run test:integration
|
run: npm run test:integration
|
||||||
28
.github/workflows/tests.unit.yaml
vendored
Normal file
28
.github/workflows/tests.unit.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ dist/
|
|||||||
.vscode
|
.vscode
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
|
# Cypress
|
||||||
|
/tests/e2e/screenshots
|
||||||
|
/tests/e2e/videos
|
||||||
|
|||||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,5 +1,64 @@
|
|||||||
# Changelog
|
# 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)
|
||||||
|
* Fix, document, unrecommend Windows browser cleanup | [5ead1a0](https://github.com/undergroundwires/privacy.sexy/commit/5ead1a087d91948890bc4ae6fea176123f18c285)
|
||||||
|
* Fix failing URL status checking integration tests | [799fb09](https://github.com/undergroundwires/privacy.sexy/commit/799fb091b8eb06c70ac0c67f2ef5385dce73501f)
|
||||||
|
* Refactor to remove "Async" function name suffix | [82c43ba](https://github.com/undergroundwires/privacy.sexy/commit/82c43ba2e37fb6e7f62ccd9bec8c5f48575f0613)
|
||||||
|
* Fix dead URLs and use forks as GitHub references | [97ddc02](https://github.com/undergroundwires/privacy.sexy/commit/97ddc027cb5395a74991cabc1d8c875ee945636d)
|
||||||
|
* Fix website not loading on Safari | [0db8cc4](https://github.com/undergroundwires/privacy.sexy/commit/0db8cc420655e01cbbed57c4658489b761a15899)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.0...0.11.1)
|
||||||
|
|
||||||
|
## 0.11.0 (2021-10-21)
|
||||||
|
|
||||||
|
* Change "grouping" to "view" | [c0c475f](https://github.com/undergroundwires/privacy.sexy/commit/c0c475ff564b23a4dabcc03ac2909207a8eb61ce)
|
||||||
|
* Tighten parameter substitution tolerance | [dcccb61](https://github.com/undergroundwires/privacy.sexy/commit/dcccb617813625c224a28242c5b965bb4cd6f189)
|
||||||
|
* Add optionality for parameters | [6a89c62](https://github.com/undergroundwires/privacy.sexy/commit/6a89c6224bdef5eb96980471f3b3935b9351b197)
|
||||||
|
* Do not collapse cards on links and code area #88 | [e73c0ad](https://github.com/undergroundwires/privacy.sexy/commit/e73c0ad1bf922b1dd3360fc5aafc3434951fa63c)
|
||||||
|
* Add scripts to disable, hide and opt-out from Siri | [c92dc1e](https://github.com/undergroundwires/privacy.sexy/commit/c92dc1e25387c65a3a41ca64d2a23cf8131b4c86)
|
||||||
|
* Improve macOS scripts for cleaning OS logs | [6c3c2e6](https://github.com/undergroundwires/privacy.sexy/commit/6c3c2e6709ec84f8e0411f19c024bab2c7e5753b)
|
||||||
|
* Add "with" expression for templating #53 | [862914b](https://github.com/undergroundwires/privacy.sexy/commit/862914b06ea9ef74c4b58a9a4164a10a38273638)
|
||||||
|
* Add support for pipes in templates #53 | [4d7ff7e](https://github.com/undergroundwires/privacy.sexy/commit/4d7ff7edc5a96cc0d99d3c1ca4fdf9bbdace3fd2)
|
||||||
|
* Bump node environment to 15.x | [2f0321f](https://github.com/undergroundwires/privacy.sexy/commit/2f0321f315ac0da8c713dd50e37032f1de194942)
|
||||||
|
* Add new UX for optionally downloading updates | [ddf417a](https://github.com/undergroundwires/privacy.sexy/commit/ddf417a16a79551b43576befab0541ea08487969)
|
||||||
|
* Add pipes to write pretty PowerShell #53 | [5217b0b](https://github.com/undergroundwires/privacy.sexy/commit/5217b0b7587ccfe509ba8adc3a7748b9bae14d7a)
|
||||||
|
* Improve alignment, padding/margin issues on UI | [c8cb7a5](https://github.com/undergroundwires/privacy.sexy/commit/c8cb7a5c28420557319606da82f56b011e88f470)
|
||||||
|
* Support disabling per-user services in Windows #16 | [4b23907](https://github.com/undergroundwires/privacy.sexy/commit/4b2390736ac1f9de2d5176b7b07da0e827112f9a)
|
||||||
|
* Add script to remove Meet Now icon in Windows | [f39ee76](https://github.com/undergroundwires/privacy.sexy/commit/f39ee76c0cda95f54502b19d5c49390fd0f12b5e)
|
||||||
|
* Add support for more depth in function calls | [20b7d28](https://github.com/undergroundwires/privacy.sexy/commit/20b7d283b02dd751dfbde18ef1fe334c6bf76e2b)
|
||||||
|
* Increase default screen width on desktop app | [9942df1](https://github.com/undergroundwires/privacy.sexy/commit/9942df16c8334ff041fb92f432a3a29e351c88df)
|
||||||
|
* Improve disabling of SmartScreen #74 | [0696ed8](https://github.com/undergroundwires/privacy.sexy/commit/0696ed8396e298a358bec17adb91c9145dd90418)
|
||||||
|
* Remove integration tests from deployments #90 | [37ad26a](https://github.com/undergroundwires/privacy.sexy/commit/37ad26a082851c02497c36e7fce40555b9480e11)
|
||||||
|
* Use a consistent color system | [b08a6b5](https://github.com/undergroundwires/privacy.sexy/commit/b08a6b5cecf4a53023053695292146edbd24b960)
|
||||||
|
* Add semi-automatic update support for macOS | [410bcd8](https://github.com/undergroundwires/privacy.sexy/commit/410bcd82445097c29c9fcf0eabf7af9ebcb93c1e)
|
||||||
|
* Add more ways to disable and clean Defender #74 | [2492f2d](https://github.com/undergroundwires/privacy.sexy/commit/2492f2d8141b3abdf590ccad59680b1f50ecb59e)
|
||||||
|
* Add privacy over security scripts for macOS #83 | [236a0f6](https://github.com/undergroundwires/privacy.sexy/commit/236a0f6c8241294fc397194cd1b20bdeccbbb50b)
|
||||||
|
* Change PowerShell double quotes escape | [9aa8166](https://github.com/undergroundwires/privacy.sexy/commit/9aa816689146ee6cd86d8262112677c38651c6bd)
|
||||||
|
* Change theme colors | [a8031d1](https://github.com/undergroundwires/privacy.sexy/commit/a8031d18d520dd3b0567f7b8cfe2dcd694b65073)
|
||||||
|
* Improve security hardening for macOS | [e6152fa](https://github.com/undergroundwires/privacy.sexy/commit/e6152fa76f5e7d23b0f79d5dd98713daaecbff90)
|
||||||
|
* Support disabling of protected services #74 | [ab8bce7](https://github.com/undergroundwires/privacy.sexy/commit/ab8bce768650a10677f0a13b3a9fae93c83802ff)
|
||||||
|
* Fix minor issues with Defender scripts | [739287a](https://github.com/undergroundwires/privacy.sexy/commit/739287ac71b3f8b04348fc101f1fa06f2d7d86a2)
|
||||||
|
* Update screenshot | [504fa05](https://github.com/undergroundwires/privacy.sexy/commit/504fa056d7d8b17fc20afd398f9a557495fca7e8)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.3...0.11.0)
|
||||||
|
|
||||||
## 0.10.3 (2021-08-27)
|
## 0.10.3 (2021-08-27)
|
||||||
|
|
||||||
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)
|
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)
|
||||||
|
|||||||
111
README.md
111
README.md
@@ -2,21 +2,104 @@
|
|||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
[](./CONTRIBUTING.md)
|
<!-- markdownlint-disable MD033 -->
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
<p align="center">
|
||||||
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
<img
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
alt="contributions are welcome"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
/>
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
</a>
|
||||||
[](https://github.com/undergroundwires/bump-everywhere)
|
<!-- 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 -->
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
||||||
- 💡 No need to run any compiled software on your computer.
|
- 💡 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.10.3/privacy.sexy-Setup-0.10.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.AppImage).
|
- 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).
|
||||||
- 💡 Single click to execute your script.
|
- 💡 Single click to execute your script.
|
||||||
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
@@ -49,6 +132,9 @@
|
|||||||
- Testing
|
- Testing
|
||||||
- Run unit tests: `npm run test:unit`
|
- Run unit tests: `npm run test:unit`
|
||||||
- Run integration tests: `npm run test:integration`
|
- 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`
|
- Lint: `npm run lint`
|
||||||
- **Desktop app**
|
- **Desktop app**
|
||||||
- Development: `npm run electron:serve`
|
- Development: `npm run electron:serve`
|
||||||
@@ -57,8 +143,8 @@
|
|||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
- Or run using Docker:
|
- Or run using Docker:
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.10.3 .`
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.2 .`
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.3 undergroundwires/privacy.sexy:0.10.3`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.2 undergroundwires/privacy.sexy:0.11.2`
|
||||||
|
|
||||||
## Architecture overview
|
## Architecture overview
|
||||||
|
|
||||||
@@ -84,5 +170,6 @@
|
|||||||
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
|
- 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
|
- 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.
|
- Everything that's merged in the master goes directly to production.
|
||||||
|
- 📖 Read more on [CI/CD pipelines](./docs/ci-cd.md)
|
||||||
|
|
||||||
[](.github/workflows/)
|
[](.github/workflows/)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/cli-plugin-babel/preset'
|
'@vue/cli-plugin-babel/preset',
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
|
|||||||
3
cypress.json
Normal file
3
cypress.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"pluginsFile": "tests/e2e/plugins/index.js"
|
||||||
|
}
|
||||||
19
docs/ci-cd.md
Normal file
19
docs/ci-cd.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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`
|
||||||
@@ -10,11 +10,16 @@
|
|||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
|
||||||
- [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components.
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that will be processed by webpack.
|
||||||
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
||||||
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
||||||
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles that are reusable and tightly coupled a Vue/HTML component.
|
||||||
|
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
|
||||||
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the only file used from other components.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
||||||
- [`electron/`](./../src/presentation/electron/): Electron configuration for the desktop application.
|
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
||||||
- [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack.
|
- [**`/public/`**](./../public/): Contains static assets that will directly be copied and not go through webpack.
|
||||||
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
|
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
|
||||||
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
|
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
|
||||||
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
|
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
|
||||||
@@ -50,3 +55,18 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
<div @click="$refs.testDialog.show()">Show dialog</div>
|
<div @click="$refs.testDialog.show()">Show dialog</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sass naming convention
|
||||||
|
|
||||||
|
- Use lowercase for variables/functions/mixins e.g.
|
||||||
|
- Variable: `$variable: value;`
|
||||||
|
- Function: `@function function() {}`
|
||||||
|
- Mixin: `@mixin mixin() {}`
|
||||||
|
- Use - for a phrase/compound word e.g.
|
||||||
|
- Variable: `$some-variable: value;`
|
||||||
|
- Function: `@function some-function() {}`
|
||||||
|
- Mixin: `@mixin some-mixin() {}`
|
||||||
|
- Grouping and name variables from generic to specific e.g.
|
||||||
|
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
|
||||||
|
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`
|
||||||
|
|
||||||
@@ -3,41 +3,59 @@
|
|||||||
- There are two different types of tests executed:
|
- There are two different types of tests executed:
|
||||||
1. [Unit tests](#unit-tests)
|
1. [Unit tests](#unit-tests)
|
||||||
2. [Integration tests](#integration-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.
|
- 💡 You can use path/module alias `@/tests` in import statements.
|
||||||
|
|
||||||
## Unit tests
|
## Unit tests
|
||||||
|
|
||||||
- Tests each component in isolation
|
- Tests each component in isolation.
|
||||||
- Defined in [`./tests/unit`](./../tests/unit)
|
- Defined in [`./tests/unit`](./../tests/unit).
|
||||||
- They follow same folder structure as [`./src`](./../src)
|
- They follow same folder structure as [`./src`](./../src).
|
||||||
|
|
||||||
### Naming
|
### Naming
|
||||||
|
|
||||||
- Each test suite first describe the system under test
|
- Each test suite first describe the system under test.
|
||||||
- E.g. tests for class `Application` is categorized under `Application`
|
- E.g. tests for class `Application` is categorized under `Application`.
|
||||||
- Tests for specific methods are categorized under method name (if applicable)
|
- Tests for specific methods are categorized under method name (if applicable).
|
||||||
- E.g. test for `run()` is categorized under `run`
|
- E.g. test for `run()` is categorized under `run`.
|
||||||
|
|
||||||
### Act, arrange, assert
|
### Act, arrange, assert
|
||||||
|
|
||||||
- Tests use act, arrange and assert (AAA) pattern when applicable
|
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
||||||
- **Arrange**
|
- **Arrange**
|
||||||
- Should set up the test case
|
- Should set up the test case.
|
||||||
- Starts with comment line `// arrange`
|
- Starts with comment line `// arrange`.
|
||||||
- **Act**
|
- **Act**
|
||||||
- Should cover the main thing to be tested
|
- Should cover the main thing to be tested.
|
||||||
- Starts with comment line `// act`
|
- Starts with comment line `// act`.
|
||||||
- **Assert**
|
- **Assert**
|
||||||
- Should elicit some sort of response
|
- Should elicit some sort of response.
|
||||||
- Starts with comment line `// assert`
|
- Starts with comment line `// assert`.
|
||||||
|
|
||||||
### Stubs
|
### Stubs
|
||||||
|
|
||||||
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
|
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs).
|
||||||
- They implement dummy behavior to be functional
|
- They implement dummy behavior to be functional.
|
||||||
|
|
||||||
## Integration tests
|
## Integration tests
|
||||||
|
|
||||||
- Tests functionality of a component in combination with others (not isolated)
|
- Tests functionality of a component in combination with others (not isolated).
|
||||||
- Ensure dependencies to third parties work as expected
|
- Ensure dependencies to third parties work as expected.
|
||||||
- Defined in [`./tests/integration`](./../tests/integration)
|
- 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.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 255 KiB |
43591
package-lock.json
generated
43591
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
92
package.json
92
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.10.3",
|
"version": "0.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
@@ -8,68 +8,86 @@
|
|||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||||
"electron:build": "vue-cli-service electron:build",
|
"electron:build": "vue-cli-service electron:build",
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
"electron:serve": "vue-cli-service electron:serve",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:vue": "vue-cli-service lint --no-fix",
|
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps",
|
||||||
|
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.3",
|
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
"@fortawesome/vue-fontawesome": "^2.0.6",
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.13",
|
||||||
"core-js": "^3.12.1",
|
"core-js": "^3.18.3",
|
||||||
"cross-fetch": "^3.1.4",
|
"cross-fetch": "^3.1.4",
|
||||||
"electron-progressbar": "^2.0.1",
|
"electron-progressbar": "^2.0.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.1.1",
|
"install": "^0.13.0",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
|
"npm": "^8.1.1",
|
||||||
"v-tooltip": "2.1.3",
|
"v-tooltip": "2.1.3",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.14",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"vue-js-modal": "^2.0.1",
|
||||||
"vue-property-decorator": "^9.1.2"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.45",
|
"@types/ace": "0.0.47",
|
||||||
"@types/chai": "^4.2.18",
|
"@types/chai": "^4.2.22",
|
||||||
"@types/file-saver": "^2.0.2",
|
"@types/file-saver": "^2.0.3",
|
||||||
"@types/mocha": "^8.2.2",
|
"@types/mocha": "^9.0.0",
|
||||||
"@vue/cli-plugin-babel": "^4.5.13",
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.13",
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.13",
|
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
|
||||||
"@vue/cli-service": "^4.5.13",
|
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
|
||||||
"@vue/test-utils": "1.2.0",
|
"@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/test-utils": "1.2.2",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"electron": "^12.0.7",
|
"cypress": "^8.3.0",
|
||||||
|
"electron": "^15.3.0",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-log": "^4.3.5",
|
"electron-log": "^4.4.1",
|
||||||
"electron-updater": "^4.3.8",
|
"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",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.27.1",
|
"markdownlint-cli": "^0.29.0",
|
||||||
"remark-cli": "^9.0.0",
|
"raw-loader": "^4.0.2",
|
||||||
|
"remark-cli": "^10.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^4.0.0",
|
"remark-preset-lint-consistent": "^5.1.0",
|
||||||
"remark-validate-links": "^10.0.4",
|
"remark-validate-links": "^11.0.1",
|
||||||
"sass": "^1.32.12",
|
"sass": "^1.43.3",
|
||||||
"sass-loader": "^10.0.1",
|
"sass-loader": "10.2.0",
|
||||||
"tslib": "^2.2.0",
|
"ts-loader": "9.0.1",
|
||||||
"typescript": "^4.2.4",
|
"tslib": "^2.3.1",
|
||||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6",
|
"typescript": "^4.4.4",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||||
|
"vue-template-compiler": "^2.6.14",
|
||||||
"yaml-lint": "^1.2.4"
|
"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",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ export type ApplicationGetter = () => IApplication;
|
|||||||
const ApplicationGetter: ApplicationGetter = parseApplication;
|
const ApplicationGetter: ApplicationGetter = parseApplication;
|
||||||
|
|
||||||
export class ApplicationFactory implements IApplicationFactory {
|
export class ApplicationFactory implements IApplicationFactory {
|
||||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||||
private readonly getter: AsyncLazy<IApplication>;
|
|
||||||
protected constructor(costlyGetter: ApplicationGetter) {
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
if (!costlyGetter) {
|
|
||||||
throw new Error('undefined getter');
|
protected constructor(costlyGetter: ApplicationGetter) {
|
||||||
}
|
if (!costlyGetter) {
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
throw new Error('undefined getter');
|
||||||
}
|
|
||||||
public getAppAsync(): Promise<IApplication> {
|
|
||||||
return this.getter.getValueAsync();
|
|
||||||
}
|
}
|
||||||
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getApp(): Promise<IApplication> {
|
||||||
|
return this.getter.getValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
// Compares to Array<T> objects for equality, ignoring order
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('undefined first array'); }
|
if (!array1) { throw new Error('undefined first array'); }
|
||||||
if (!array2) { throw new Error('undefined second array'); }
|
if (!array2) { throw new Error('undefined second array'); }
|
||||||
const sortedArray1 = sort(array1);
|
const sortedArray1 = sort(array1);
|
||||||
const sortedArray2 = sort(array2);
|
const sortedArray2 = sort(array2);
|
||||||
return sequenceEqual(sortedArray1, sortedArray2);
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
function sort(array: readonly T[]) {
|
function sort(array: readonly T[]) {
|
||||||
return array.slice().sort();
|
return array.slice().sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compares to Array<T> objects for equality in same order
|
// Compares to Array<T> objects for equality in same order
|
||||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('undefined first array'); }
|
if (!array1) { throw new Error('undefined first array'); }
|
||||||
if (!array2) { throw new Error('undefined second array'); }
|
if (!array2) { throw new Error('undefined second array'); }
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return array1.every((val, index) => val === array2[index]);
|
return array1.every((val, index) => val === array2[index]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,63 @@
|
|||||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||||
export type EnumType = number | string;
|
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> {
|
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
return Object
|
): IEnumParser<TEnumValue> {
|
||||||
.values(enumVariable)
|
return {
|
||||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
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>(
|
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
return getEnumNames(enumVariable)
|
): TEnumValue[] {
|
||||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
return getEnumNames(enumVariable)
|
||||||
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>) {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
if (value === undefined) {
|
) {
|
||||||
throw new Error('undefined enum value');
|
if (value === undefined) {
|
||||||
}
|
throw new Error('undefined enum value');
|
||||||
if (!(value in enumVariable)) {
|
}
|
||||||
throw new RangeError(`enum value "${value}" is out of range`);
|
if (!(value in enumVariable)) {
|
||||||
}
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
export interface IScriptingLanguageFactory<T> {
|
export interface IScriptingLanguageFactory<T> {
|
||||||
create(language: ScriptingLanguage): T;
|
create(language: ScriptingLanguage): T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||||
|
|
||||||
type Getter<T> = () => T;
|
type Getter<T> = () => T;
|
||||||
|
|
||||||
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<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 {
|
public create(language: ScriptingLanguage): T {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!this.getters.has(language)) {
|
if (!this.getters.has(language)) {
|
||||||
throw new RangeError(`unknown language: "${ScriptingLanguage[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>) {
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!getter) {
|
if (!getter) {
|
||||||
throw new Error('undefined 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,64 @@
|
|||||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
|
||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
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>;
|
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||||
|
|
||||||
export class ApplicationContext implements IApplicationContext {
|
export class ApplicationContext implements IApplicationContext {
|
||||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||||
public collection: ICategoryCollection;
|
|
||||||
public currentOs: OperatingSystem;
|
|
||||||
|
|
||||||
public get state(): ICategoryCollectionState {
|
public collection: ICategoryCollection;
|
||||||
return this.states[this.collection.os];
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly states: StateMachine;
|
public currentOs: OperatingSystem;
|
||||||
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 {
|
public get state(): ICategoryCollectionState {
|
||||||
if (this.currentOs === os) {
|
return this.states[this.collection.os];
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
this.collection = this.app.getCollection(os);
|
private readonly states: StateMachine;
|
||||||
if (!this.collection) {
|
|
||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
public constructor(
|
||||||
}
|
public readonly app: IApplication,
|
||||||
const event: IApplicationContextChangedEvent = {
|
initialContext: OperatingSystem,
|
||||||
newState: this.states[os],
|
) {
|
||||||
oldState: this.states[this.currentOs],
|
validateApp(app);
|
||||||
};
|
assertInRange(initialContext, OperatingSystem);
|
||||||
this.contextChanged.notify(event);
|
this.states = initializeStates(app);
|
||||||
this.currentOs = os;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApp(app: IApplication) {
|
function validateApp(app: IApplication) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new Error('undefined app');
|
throw new Error('undefined app');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
machine[collection.os] = new CategoryCollectionState(collection);
|
machine[collection.os] = new CategoryCollectionState(collection);
|
||||||
}
|
}
|
||||||
return machine;
|
return machine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import { ApplicationContext } from './ApplicationContext';
|
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { Environment } from '../Environment/Environment';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { Environment } from '../Environment/Environment';
|
||||||
import { IEnvironment } from '../Environment/IEnvironment';
|
import { IEnvironment } from '../Environment/IEnvironment';
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
import { IApplicationFactory } from '../IApplicationFactory';
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
|
||||||
export async function buildContextAsync(
|
export async function buildContext(
|
||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
|
environment = Environment.CurrentEnvironment,
|
||||||
if (!factory) { throw new Error('undefined factory'); }
|
): Promise<IApplicationContext> {
|
||||||
if (!environment) { throw new Error('undefined environment'); }
|
if (!factory) { throw new Error('undefined factory'); }
|
||||||
const app = await factory.getAppAsync();
|
if (!environment) { throw new Error('undefined environment'); }
|
||||||
const os = getInitialOs(app, environment);
|
const app = await factory.getApp();
|
||||||
return new ApplicationContext(app, os);
|
const os = getInitialOs(app, environment);
|
||||||
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||||
const currentOs = environment.os;
|
const currentOs = environment.os;
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
return getPriority(os2) - getPriority(os1);
|
return getPriority(os2) - getPriority(os1);
|
||||||
});
|
});
|
||||||
return supportedOsList[0];
|
return supportedOsList[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
|
||||||
export interface IApplicationContext {
|
export interface IReadOnlyApplicationContext {
|
||||||
readonly app: IApplication;
|
readonly app: IApplication;
|
||||||
|
readonly state: IReadOnlyCategoryCollectionState;
|
||||||
|
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||||
readonly state: ICategoryCollectionState;
|
readonly state: ICategoryCollectionState;
|
||||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
|
||||||
changeContext(os: OperatingSystem): void;
|
changeContext(os: OperatingSystem): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApplicationContextChangedEvent {
|
export interface IApplicationContextChangedEvent {
|
||||||
readonly newState: ICategoryCollectionState;
|
readonly newState: ICategoryCollectionState;
|
||||||
readonly oldState: ICategoryCollectionState;
|
readonly oldState: ICategoryCollectionState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { UserFilter } from './Filter/UserFilter';
|
import { UserFilter } from './Filter/UserFilter';
|
||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
@@ -5,19 +7,20 @@ import { UserSelection } from './Selection/UserSelection';
|
|||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
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 constructor(readonly collection: ICategoryCollection) {
|
public readonly code: IApplicationCode;
|
||||||
this.selection = new UserSelection(collection, []);
|
|
||||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
public readonly selection: IUserSelection;
|
||||||
this.filter = new UserFilter(collection);
|
|
||||||
this.os = collection.os;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
|
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 { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
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 { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||||
public current: string;
|
|
||||||
|
|
||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
public current: string;
|
||||||
|
|
||||||
constructor(
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
constructor(
|
||||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
userSelection: IReadOnlyUserSelection,
|
||||||
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
this.current = code.code;
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
this.scriptPositions = code.scriptPositions;
|
) {
|
||||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||||
this.changed.notify(event);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,71 @@
|
|||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
|
||||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||||
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
|
||||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
|
||||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
|
||||||
|
|
||||||
private readonly scripts: Map<IScript, ICodePosition>;
|
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
constructor(
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
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 {
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
return this.scripts.size === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
return this.scripts.get(script);
|
|
||||||
}
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
for (const position of positions) {
|
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||||
if (position.endLine > totalLines) {
|
if (missingPositions.length > 0) {
|
||||||
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
throw new Error(
|
||||||
`(total code lines: ${totalLines}`);
|
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||||
}
|
+ `(total code lines: ${totalLines}).`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChangedScripts(
|
function getChangedScripts(
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
newScripts: ReadonlyArray<SelectedScript>,
|
||||||
return newScripts
|
): ReadonlyArray<IScript> {
|
||||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
return newScripts
|
||||||
&& oldScript.revert !== newScript.revert ))
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
.map((selection) => selection.script);
|
&& oldScript.revert !== newScript.revert))
|
||||||
|
.map((selection) => selection.script);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectIfNotExists(
|
function selectIfNotExists(
|
||||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||||
test: ReadonlyArray<SelectedScript>) {
|
test: ReadonlyArray<SelectedScript>,
|
||||||
return selectableContainer
|
) {
|
||||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
return selectableContainer
|
||||||
.map((selection) => selection.script);
|
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||||
|
.map((selection) => selection.script);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { IScript } from '@/domain/IScript';
|
|||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
addedScripts: ReadonlyArray<IScript>;
|
addedScripts: ReadonlyArray<IScript>;
|
||||||
removedScripts: ReadonlyArray<IScript>;
|
removedScripts: ReadonlyArray<IScript>;
|
||||||
changedScripts: ReadonlyArray<IScript>;
|
changedScripts: ReadonlyArray<IScript>;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,64 +4,65 @@ const NewLine = '\n';
|
|||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
export abstract class CodeBuilder implements ICodeBuilder {
|
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)
|
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||||
public get currentLine(): number {
|
public get currentLine(): number {
|
||||||
return this.lines.length;
|
return this.lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendLine(code?: string): CodeBuilder {
|
||||||
|
if (!code) {
|
||||||
|
this.lines.push('');
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
|
this.lines.push(...lines);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public appendLine(code?: string): CodeBuilder {
|
public appendTrailingHyphensCommentLine(
|
||||||
if (!code) {
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||||
this.lines.push('');
|
): CodeBuilder {
|
||||||
return this;
|
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
|
||||||
for (const line of lines) {
|
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||||
this.lines.push(line);
|
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
||||||
}
|
return this;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
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 appendTrailingHyphensCommentLine(
|
public toString(): string {
|
||||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
return this.lines.join(NewLine);
|
||||||
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public appendCommentLine(commentLine?: string): CodeBuilder {
|
protected abstract getCommentDelimiter(): string;
|
||||||
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public appendFunction(name: string, code: string): CodeBuilder {
|
protected abstract writeStandardOut(text: string): string;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { BatchBuilder } from './Languages/BatchBuilder';
|
|||||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
|
||||||
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
|
export class CodeBuilderFactory
|
||||||
constructor() {
|
extends ScriptingLanguageFactory<ICodeBuilder>
|
||||||
super();
|
implements ICodeBuilderFactory {
|
||||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
constructor() {
|
||||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
super();
|
||||||
}
|
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||||
|
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export interface ICodeBuilder {
|
export interface ICodeBuilder {
|
||||||
currentLine: number;
|
currentLine: number;
|
||||||
appendLine(code?: string): ICodeBuilder;
|
appendLine(code?: string): ICodeBuilder;
|
||||||
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
||||||
appendCommentLine(commentLine?: string): ICodeBuilder;
|
appendCommentLine(commentLine?: string): ICodeBuilder;
|
||||||
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
||||||
appendFunction(name: string, code: string): ICodeBuilder;
|
appendFunction(name: string, code: string): ICodeBuilder;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ICodeBuilder } from './ICodeBuilder';
|
|
||||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
|
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
|
|||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface IUserScript {
|
export interface IUserScript {
|
||||||
code: string;
|
code: string;
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(
|
buildCode(
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
scriptingDefinition: IScriptingDefinition): IUserScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
export class BatchBuilder extends CodeBuilder {
|
export class BatchBuilder extends CodeBuilder {
|
||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '::';
|
return '::';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
|
||||||
return `echo ${escapeForEcho(text)}`;
|
protected writeStandardOut(text: string): string {
|
||||||
}
|
return `echo ${escapeForEcho(text)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
return text
|
return text
|
||||||
.replace(/&/g, '^&')
|
.replace(/&/g, '^&')
|
||||||
.replace(/%/g, '%%');
|
.replace(/%/g, '%%');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
export class ShellBuilder extends CodeBuilder {
|
export class ShellBuilder extends CodeBuilder {
|
||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '#';
|
return '#';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
|
||||||
return `echo '${escapeForEcho(text)}'`;
|
protected writeStandardOut(text: string): string {
|
||||||
}
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
return text
|
return text
|
||||||
.replace(/'/g, '\'\\\'\'');
|
.replace(/'/g, '\'\\\'\'');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,75 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||||
|
|
||||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
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>() };
|
||||||
}
|
}
|
||||||
public buildCode(
|
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
const scriptPositions = selectedScripts.reduce((result, selection) => {
|
||||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
return appendSelection(selection, result, builder);
|
||||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
}, new Map<SelectedScript, ICodePosition>());
|
||||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||||
if (!selectedScripts.length) {
|
return { code, scriptPositions };
|
||||||
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 {
|
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
|
||||||
if (!startCode) {
|
if (!startCode) {
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
return builder
|
return builder
|
||||||
.appendLine(startCode)
|
.appendLine(startCode)
|
||||||
.appendLine();
|
.appendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
||||||
if (!endCode) {
|
if (!endCode) {
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
return builder.appendLine()
|
return builder.appendLine()
|
||||||
.appendLine(endCode)
|
.appendLine(endCode)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendSelection(
|
function appendSelection(
|
||||||
selection: SelectedScript,
|
selection: SelectedScript,
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
builder: ICodeBuilder,
|
||||||
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
): Map<SelectedScript, ICodePosition> {
|
||||||
builder = appendCode(selection, builder);
|
// Start from next line because first line will be empty to separate scripts
|
||||||
const endPosition = builder.currentLine - 1;
|
const startPosition = builder.currentLine + 1;
|
||||||
builder.appendLine();
|
appendCode(selection, builder);
|
||||||
const position = new CodePosition(startPosition, endPosition);
|
const endPosition = builder.currentLine - 1;
|
||||||
scriptPositions.set(selection, position);
|
builder.appendLine();
|
||||||
return scriptPositions;
|
const position = new CodePosition(startPosition, endPosition);
|
||||||
|
scriptPositions.set(selection, position);
|
||||||
|
return scriptPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
const { script } = selection;
|
||||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||||
return builder
|
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
||||||
.appendLine()
|
return builder
|
||||||
.appendFunction(name, scriptCode);
|
.appendLine()
|
||||||
|
.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
|
|
||||||
export interface IApplicationCode {
|
export interface IApplicationCode {
|
||||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||||
readonly current: string;
|
readonly current: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { ICodePosition } from './ICodePosition';
|
import { ICodePosition } from './ICodePosition';
|
||||||
|
|
||||||
export class CodePosition implements ICodePosition {
|
export class CodePosition implements ICodePosition {
|
||||||
public get totalLines(): number {
|
public get totalLines(): number {
|
||||||
return this.endLine - this.startLine;
|
return this.endLine - this.startLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly startLine: number,
|
public readonly startLine: number,
|
||||||
public readonly endLine: number) {
|
public readonly endLine: number,
|
||||||
if (startLine < 0) {
|
) {
|
||||||
throw new Error('Code cannot start in a negative line');
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface ICodePosition {
|
export interface ICodePosition {
|
||||||
readonly startLine: number;
|
readonly startLine: number;
|
||||||
readonly endLine: number;
|
readonly endLine: number;
|
||||||
readonly totalLines: number;
|
readonly totalLines: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { IFilterResult } from './IFilterResult';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
|
||||||
export class FilterResult implements IFilterResult {
|
export class FilterResult implements IFilterResult {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||||
public readonly query: string) {
|
public readonly query: string,
|
||||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
) {
|
||||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
if (!categoryMatches) { throw new Error('Category matches is 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;
|
public hasAnyMatches(): boolean {
|
||||||
}
|
return this.scriptMatches.length > 0
|
||||||
|
|| this.categoryMatches.length > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { IScript, ICategory } from '@/domain/ICategory';
|
import { IScript, ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
export interface IFilterResult {
|
export interface IFilterResult {
|
||||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||||
readonly scriptMatches: ReadonlyArray<IScript>;
|
readonly scriptMatches: ReadonlyArray<IScript>;
|
||||||
readonly query: string;
|
readonly query: string;
|
||||||
hasAnyMatches(): boolean;
|
hasAnyMatches(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IReadOnlyUserFilter {
|
||||||
readonly currentFilter: IFilterResult | undefined;
|
readonly currentFilter: IFilterResult | undefined;
|
||||||
readonly filtered: IEventSource<IFilterResult>;
|
readonly filtered: IEventSource<IFilterResult>;
|
||||||
readonly filterRemoved: IEventSource<void>;
|
readonly filterRemoved: IEventSource<void>;
|
||||||
setFilter(filter: string): void;
|
}
|
||||||
removeFilter(): void;
|
|
||||||
|
export interface IUserFilter extends IReadOnlyUserFilter {
|
||||||
|
setFilter(filter: string): void;
|
||||||
|
removeFilter(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,56 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { FilterResult } from './FilterResult';
|
import { FilterResult } from './FilterResult';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { IUserFilter } from './IUserFilter';
|
import { IUserFilter } from './IUserFilter';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
|
|
||||||
export class UserFilter implements IUserFilter {
|
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;
|
|
||||||
|
|
||||||
constructor(private collection: ICategoryCollection) {
|
public readonly filterRemoved = new EventSource<void>();
|
||||||
|
|
||||||
|
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 setFilter(filter: string): void {
|
public removeFilter(): void {
|
||||||
if (!filter) {
|
this.currentFilter = undefined;
|
||||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
this.filterRemoved.notify();
|
||||||
}
|
}
|
||||||
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) {
|
function isScriptAMatch(script: IScript, filterLowercase: string) {
|
||||||
if (script.name.toLowerCase().includes(filterLowercase)) {
|
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.code.revert) {
|
if (script.code.revert) {
|
||||||
return script.code.revert.toLowerCase().includes(filterLowercase);
|
return script.code.revert.toLowerCase().includes(filterLowercase);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { IUserFilter } from './Filter/IUserFilter';
|
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||||
|
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
||||||
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
|
||||||
export interface ICategoryCollectionState {
|
export interface IReadOnlyCategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
readonly filter: IUserFilter;
|
readonly os: OperatingSystem;
|
||||||
readonly selection: IUserSelection;
|
readonly filter: IReadOnlyUserFilter;
|
||||||
readonly collection: ICategoryCollection;
|
readonly selection: IReadOnlyUserSelection;
|
||||||
readonly os: OperatingSystem;
|
readonly collection: ICategoryCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||||
|
readonly filter: IUserFilter;
|
||||||
|
readonly selection: IUserSelection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IReadOnlyUserSelection {
|
||||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
areAllSelected(category: ICategory): boolean;
|
isSelected(scriptId: string): boolean;
|
||||||
isAnySelected(category: ICategory): boolean;
|
areAllSelected(category: ICategory): boolean;
|
||||||
removeAllInCategory(categoryId: number): void;
|
isAnySelected(category: ICategory): boolean;
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
}
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
export interface IUserSelection extends IReadOnlyUserSelection {
|
||||||
removeSelectedScript(scriptId: string): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
isSelected(scriptId: string): boolean;
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
selectAll(): void;
|
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
deselectAll(): void;
|
removeSelectedScript(scriptId: string): void;
|
||||||
|
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||||
|
selectAll(): void;
|
||||||
|
deselectAll(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
export class SelectedScript extends BaseEntity<string> {
|
export class SelectedScript extends BaseEntity<string> {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: IScript,
|
public readonly script: IScript,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
super(script.id);
|
super(script.id);
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error('cannot revert an irreversible script');
|
throw new Error('cannot revert an irreversible script');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +1,146 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
import { IUserSelection } from './IUserSelection';
|
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { IUserSelection } from './IUserSelection';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
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>;
|
|
||||||
|
|
||||||
constructor(
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public areAllSelected(category: ICategory): boolean {
|
constructor(
|
||||||
if (this.selectedScripts.length === 0) {
|
private readonly collection: ICategoryCollection,
|
||||||
return false;
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
}
|
) {
|
||||||
const scripts = category.getAllScriptsRecursively();
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (this.selectedScripts.length < scripts.length) {
|
for (const script of selectedScripts) {
|
||||||
return false;
|
this.scripts.addItem(script);
|
||||||
}
|
|
||||||
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public isAnySelected(category: ICategory): boolean {
|
public areAllSelected(category: ICategory): boolean {
|
||||||
if (this.selectedScripts.length === 0) {
|
if (this.selectedScripts.length === 0) {
|
||||||
return false;
|
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 removeAllInCategory(categoryId: number): void {
|
public isAnySelected(category: ICategory): boolean {
|
||||||
const category = this.collection.findCategory(categoryId);
|
if (this.selectedScripts.length === 0) {
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
return false;
|
||||||
.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 addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
public removeAllInCategory(categoryId: number): void {
|
||||||
const category = this.collection.findCategory(categoryId);
|
const category = this.collection.findCategory(categoryId);
|
||||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
.filter((script) =>
|
.filter((script) => this.scripts.exists(script.id));
|
||||||
!this.scripts.exists(script.id)
|
if (!scriptsToRemove.length) {
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
return;
|
||||||
);
|
|
||||||
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 addSelectedScript(scriptId: string, revert: boolean): void {
|
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||||
const script = this.collection.findScript(scriptId);
|
const scriptsToAddOrUpdate = this.collection
|
||||||
if (!script) {
|
.findCategory(categoryId)
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
.getAllScriptsRecursively()
|
||||||
}
|
.filter(
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
(script) => !this.scripts.exists(script.id)
|
||||||
this.scripts.addItem(selectedScript);
|
|| this.scripts.getById(script.id).revert !== revert,
|
||||||
this.changed.notify(this.scripts.getItems());
|
)
|
||||||
|
.map((script) => new SelectedScript(script, revert));
|
||||||
|
if (!scriptsToAddOrUpdate.length) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
for (const script of scriptsToAddOrUpdate) {
|
||||||
|
this.scripts.addOrUpdateItem(script);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.collection.findScript(scriptId);
|
const script = this.collection.findScript(scriptId);
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
if (!script) {
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
}
|
||||||
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public removeSelectedScript(scriptId: string): void {
|
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
this.scripts.removeItem(scriptId);
|
const script = this.collection.findScript(scriptId);
|
||||||
this.changed.notify(this.scripts.getItems());
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
}
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public isSelected(scriptId: string): boolean {
|
public removeSelectedScript(scriptId: string): void {
|
||||||
return this.scripts.exists(scriptId);
|
this.scripts.removeItem(scriptId);
|
||||||
}
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
public isSelected(scriptId: string): boolean {
|
||||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
return this.scripts.exists(scriptId);
|
||||||
return this.scripts.getItems();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public selectAll(): void {
|
/** Get users scripts based on his/her selections */
|
||||||
for (const script of this.collection.getAllScripts()) {
|
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||||
if (!this.scripts.exists(script.id)) {
|
return this.scripts.getItems();
|
||||||
const selection = new SelectedScript(script, false);
|
}
|
||||||
this.scripts.addItem(selection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public deselectAll(): void {
|
public selectAll(): void {
|
||||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
const scriptsToSelect = this.collection
|
||||||
for (const scriptId of selectedScriptIds) {
|
.getAllScripts()
|
||||||
this.scripts.removeItem(scriptId);
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
}
|
.map((script) => new SelectedScript(script, false));
|
||||||
this.changed.notify([]);
|
for (const script of scriptsToSelect) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public selectOnly(scripts: readonly IScript[]): void {
|
public deselectAll(): void {
|
||||||
if (!scripts || scripts.length === 0) {
|
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
for (const scriptId of selectedScriptIds) {
|
||||||
}
|
this.scripts.removeItem(scriptId);
|
||||||
// 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([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
.map((script) => new SelectedScript(script, false));
|
||||||
|
for (const toSelect of unselectedScripts) {
|
||||||
|
this.scripts.addItem(toSelect);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,52 +3,55 @@ import { DetectorBuilder } from './DetectorBuilder';
|
|||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
export class BrowserOsDetector implements IBrowserOsDetector {
|
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||||
private readonly detectors = BrowserDetectors;
|
private readonly detectors = BrowserDetectors;
|
||||||
public detect(userAgent: string): OperatingSystem | undefined {
|
|
||||||
if (!userAgent) {
|
public detect(userAgent: string): OperatingSystem | undefined {
|
||||||
return 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
|
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||||
const BrowserDetectors =
|
const BrowserDetectors = [
|
||||||
[
|
define(OperatingSystem.KaiOS, (b) => b
|
||||||
define(OperatingSystem.KaiOS, (b) =>
|
.mustInclude('KAIOS')),
|
||||||
b.mustInclude('KAIOS')),
|
define(OperatingSystem.ChromeOS, (b) => b
|
||||||
define(OperatingSystem.ChromeOS, (b) =>
|
.mustInclude('CrOS')),
|
||||||
b.mustInclude('CrOS')),
|
define(OperatingSystem.BlackBerryOS, (b) => b
|
||||||
define(OperatingSystem.BlackBerryOS, (b) =>
|
.mustInclude('BlackBerry')),
|
||||||
b.mustInclude('BlackBerry')),
|
define(OperatingSystem.BlackBerryTabletOS, (b) => b
|
||||||
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
.mustInclude('RIM Tablet OS')),
|
||||||
b.mustInclude('RIM Tablet OS')),
|
define(OperatingSystem.BlackBerry, (b) => b
|
||||||
define(OperatingSystem.BlackBerry, (b) =>
|
.mustInclude('BB10')),
|
||||||
b.mustInclude('BB10')),
|
define(OperatingSystem.Android, (b) => b
|
||||||
define(OperatingSystem.Android, (b) =>
|
.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.Android, (b) => b
|
||||||
define(OperatingSystem.Android, (b) =>
|
.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.iOS, (b) => b
|
||||||
define(OperatingSystem.iOS, (b) =>
|
.mustInclude('like Mac OS X')),
|
||||||
b.mustInclude('like Mac OS X')),
|
define(OperatingSystem.Linux, (b) => b
|
||||||
define(OperatingSystem.Linux, (b) =>
|
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||||
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
define(OperatingSystem.Windows, (b) => b
|
||||||
define(OperatingSystem.Windows, (b) =>
|
.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.WindowsPhone, (b) => b
|
||||||
define(OperatingSystem.WindowsPhone, (b) =>
|
.mustInclude('Windows Phone')),
|
||||||
b.mustInclude('Windows Phone')),
|
define(OperatingSystem.macOS, (b) => b
|
||||||
define(OperatingSystem.macOS, (b) =>
|
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||||
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
function define(
|
||||||
const builder = new DetectorBuilder(os);
|
os: OperatingSystem,
|
||||||
applyRules(builder);
|
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
|
||||||
return builder.build();
|
): IBrowserOsDetector {
|
||||||
|
const builder = new DetectorBuilder(os);
|
||||||
|
applyRules(builder);
|
||||||
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,54 @@
|
|||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
export class DetectorBuilder {
|
export class DetectorBuilder {
|
||||||
private readonly existingPartsInUserAgent = new Array<string>();
|
private readonly existingPartsInUserAgent = new Array<string>();
|
||||||
private readonly notExistingPartsInUserAgent = new Array<string>();
|
|
||||||
|
|
||||||
constructor(private readonly os: OperatingSystem) { }
|
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||||
|
|
||||||
public mustInclude(str: string): DetectorBuilder {
|
constructor(private readonly os: OperatingSystem) { }
|
||||||
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 build(): IBrowserOsDetector {
|
||||||
|
if (!this.existingPartsInUserAgent.length) {
|
||||||
|
throw new Error('Must include at least a part');
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
detect: (agent) => this.detect(agent),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public mustNotInclude(str: string): DetectorBuilder {
|
private detect(userAgent: string): OperatingSystem {
|
||||||
return this.add(str, this.notExistingPartsInUserAgent);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public build(): IBrowserOsDetector {
|
private add(part: string, array: string[]): DetectorBuilder {
|
||||||
if (!this.existingPartsInUserAgent.length) {
|
if (!part) {
|
||||||
throw new Error('Must include at least a part');
|
throw new Error('part is empty or undefined');
|
||||||
}
|
|
||||||
return {
|
|
||||||
detect: (agent) => this.detect(agent),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (this.existingPartsInUserAgent.includes(part)) {
|
||||||
private detect(userAgent: string): OperatingSystem {
|
throw new Error(`part ${part} is already included as existing part`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
if (this.notExistingPartsInUserAgent.includes(part)) {
|
||||||
private add(part: string, array: string[]): DetectorBuilder {
|
throw new Error(`part ${part} is already included as not existing part`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
array.push(part);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IBrowserOsDetector {
|
export interface IBrowserOsDetector {
|
||||||
detect(userAgent: string): OperatingSystem | undefined;
|
detect(userAgent: string): OperatingSystem | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,89 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
import { IEnvironment } from './IEnvironment';
|
import { IEnvironment } from './IEnvironment';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
interface IEnvironmentVariables {
|
export interface IEnvironmentVariables {
|
||||||
readonly window: Window & typeof globalThis;
|
readonly window: Window & typeof globalThis;
|
||||||
readonly process: NodeJS.Process;
|
readonly process: NodeJS.Process;
|
||||||
readonly navigator: Navigator;
|
readonly navigator: Navigator;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Environment implements IEnvironment {
|
export class Environment implements IEnvironment {
|
||||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||||
window,
|
window,
|
||||||
process,
|
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||||
navigator,
|
navigator,
|
||||||
});
|
});
|
||||||
public readonly isDesktop: boolean;
|
|
||||||
public readonly os: OperatingSystem;
|
public readonly isDesktop: boolean;
|
||||||
protected constructor(
|
|
||||||
variables: IEnvironmentVariables,
|
public readonly os: OperatingSystem;
|
||||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
|
||||||
if (!variables) {
|
protected constructor(
|
||||||
throw new Error('variables is null or empty');
|
variables: IEnvironmentVariables,
|
||||||
}
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||||
this.isDesktop = isDesktop(variables);
|
) {
|
||||||
this.os = this.isDesktop ?
|
if (!variables) {
|
||||||
getDesktopOsType(getProcessPlatform(variables))
|
throw new Error('variables is null or empty');
|
||||||
: 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 {
|
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||||
if (!variables.window || !variables.window.navigator) {
|
if (!variables.window || !variables.window.navigator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return variables.window.navigator.userAgent;
|
return variables.window.navigator.userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||||
if (!variables.process || !variables.process.platform) {
|
if (!variables.process || !variables.process.platform) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return variables.process.platform;
|
return variables.process.platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||||
// https://nodejs.org/api/process.html#process_process_platform
|
// https://nodejs.org/api/process.html#process_process_platform
|
||||||
if (processPlatform === 'darwin') {
|
switch (processPlatform) {
|
||||||
return OperatingSystem.macOS;
|
case 'darwin':
|
||||||
} else if (processPlatform === 'win32') {
|
return OperatingSystem.macOS;
|
||||||
return OperatingSystem.Windows;
|
case 'win32':
|
||||||
} else if (processPlatform === 'linux') {
|
return OperatingSystem.Windows;
|
||||||
return OperatingSystem.Linux;
|
case 'linux':
|
||||||
}
|
return OperatingSystem.Linux;
|
||||||
return undefined;
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
// More: https://github.com/electron/electron/issues/2288
|
// More: https://github.com/electron/electron/issues/2288
|
||||||
// Renderer process
|
// Renderer process
|
||||||
if (variables.window
|
if (variables.window
|
||||||
&& variables.window.process
|
&& variables.window.process
|
||||||
&& variables.window.process.type === 'renderer') {
|
&& variables.window.process.type === 'renderer') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Main process
|
// Main process
|
||||||
if (variables.process
|
if (variables.process
|
||||||
&& variables.process.versions
|
&& variables.process.versions
|
||||||
&& Boolean(variables.process.versions.electron)) {
|
&& Boolean(variables.process.versions.electron)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Detect the user agent when the `nodeIntegration` option is set to true
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||||
if (variables.navigator
|
if (variables.navigator
|
||||||
&& variables.navigator.userAgent
|
&& variables.navigator.userAgent
|
||||||
&& variables.navigator.userAgent.includes('Electron')) {
|
&& variables.navigator.userAgent.includes('Electron')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IEnvironment {
|
export interface IEnvironment {
|
||||||
readonly isDesktop: boolean;
|
readonly isDesktop: boolean;
|
||||||
readonly os: OperatingSystem;
|
readonly os: OperatingSystem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
export interface IApplicationFactory {
|
export interface IApplicationFactory {
|
||||||
getAppAsync(): Promise<IApplication>;
|
getApp(): Promise<IApplication>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,41 @@
|
|||||||
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
|
||||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
||||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
parser = CategoryCollectionParser,
|
parser = CategoryCollectionParser,
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
collectionsData = PreParsedCollections): IApplication {
|
collectionsData = PreParsedCollections,
|
||||||
validateCollectionsData(collectionsData);
|
): IApplication {
|
||||||
const information = parseProjectInformation(processEnv);
|
validateCollectionsData(collectionsData);
|
||||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
const information = parseProjectInformation(processEnv);
|
||||||
const app = new Application(information, collections);
|
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||||
return app;
|
const app = new Application(information, collections);
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryCollectionParserType
|
export type CategoryCollectionParserType
|
||||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||||
|
|
||||||
const CategoryCollectionParser: CategoryCollectionParserType
|
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||||
= (file, info) => parseCategoryCollection(file, info);
|
return parseCategoryCollection(file, info);
|
||||||
|
};
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData []
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
= [ WindowsData, MacOsData ];
|
WindowsData, MacOsData,
|
||||||
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections.length) {
|
if (!collections.length) {
|
||||||
throw new Error('no collection provided');
|
throw new Error('no collection provided');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
if (collections.some((collection) => !collection)) {
|
||||||
throw new Error('undefined collection provided');
|
throw new Error('undefined collection provided');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
import { Category } from '@/domain/Category';
|
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { parseCategory } from './CategoryParser';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { createEnumParser } from '../Common/Enum';
|
||||||
|
import { parseCategory } from './CategoryParser';
|
||||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export function parseCategoryCollection(
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
osParser = createEnumParser(OperatingSystem),
|
||||||
validate(content);
|
): ICategoryCollection {
|
||||||
const scripting = new ScriptingDefinitionParser()
|
validate(content);
|
||||||
.parse(content.scripting, info);
|
const scripting = new ScriptingDefinitionParser()
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
.parse(content.scripting, info);
|
||||||
const categories = new Array<Category>();
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
for (const action of content.actions) {
|
const categories = content.actions.map((action) => parseCategory(action, context));
|
||||||
const category = parseCategory(action, context);
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
categories.push(category);
|
const collection = new CategoryCollection(
|
||||||
}
|
os,
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
categories,
|
||||||
const collection = new CategoryCollection(
|
scripting,
|
||||||
os,
|
);
|
||||||
categories,
|
return collection;
|
||||||
scripting);
|
|
||||||
return collection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validate(content: CollectionData): void {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new Error('content is null or undefined');
|
throw new Error('content is null or undefined');
|
||||||
}
|
}
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
if (!content.actions || content.actions.length <= 0) {
|
||||||
throw new Error('content does not define any action');
|
throw new Error('content does not define any action');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,86 @@
|
|||||||
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
import {
|
||||||
|
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
||||||
|
} from 'js-yaml-loader!@/*';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter = 0;
|
||||||
|
|
||||||
interface ICategoryChildren {
|
export function parseCategory(
|
||||||
subCategories: Category[];
|
category: CategoryData,
|
||||||
subScripts: Script[];
|
context: ICategoryCollectionParseContext,
|
||||||
}
|
): Category {
|
||||||
|
if (!context) { throw new Error('undefined context'); }
|
||||||
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
ensureValid(category);
|
||||||
if (!context) { throw new Error('undefined context'); }
|
const children: ICategoryChildren = {
|
||||||
ensureValid(category);
|
subCategories: new Array<Category>(),
|
||||||
const children: ICategoryChildren = {
|
subScripts: new Array<Script>(),
|
||||||
subCategories: new Array<Category>(),
|
};
|
||||||
subScripts: new Array<Script>(),
|
for (const data of category.children) {
|
||||||
};
|
parseCategoryChild(data, children, category, context);
|
||||||
for (const data of category.children) {
|
}
|
||||||
parseCategoryChild(data, children, category, context);
|
return new Category(
|
||||||
}
|
/* id: */ categoryIdCounter++,
|
||||||
return new Category(
|
/* name: */ category.category,
|
||||||
/*id*/ categoryIdCounter++,
|
/* docs: */ parseDocUrls(category),
|
||||||
/*name*/ category.category,
|
/* categories: */ children.subCategories,
|
||||||
/*docs*/ parseDocUrls(category),
|
/* scripts: */ children.subScripts,
|
||||||
/*categories*/ children.subCategories,
|
);
|
||||||
/*scripts*/ children.subScripts,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValid(category: CategoryData) {
|
function ensureValid(category: CategoryData) {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw Error('category is null or undefined');
|
throw Error('category is null or undefined');
|
||||||
}
|
}
|
||||||
if (!category.children || category.children.length === 0) {
|
if (!category.children || category.children.length === 0) {
|
||||||
throw Error(`category has no children: "${category.category}"`);
|
throw Error(`category has no children: "${category.category}"`);
|
||||||
}
|
}
|
||||||
if (!category.category || category.category.length === 0) {
|
if (!category.category || category.category.length === 0) {
|
||||||
throw Error('category has no name');
|
throw Error('category has no name');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICategoryChildren {
|
||||||
|
subCategories: Category[];
|
||||||
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
data: CategoryOrScriptData,
|
data: CategoryOrScriptData,
|
||||||
children: ICategoryChildren,
|
children: ICategoryChildren,
|
||||||
parent: CategoryData,
|
parent: CategoryData,
|
||||||
context: ICategoryCollectionParseContext) {
|
context: ICategoryCollectionParseContext,
|
||||||
if (isCategory(data)) {
|
) {
|
||||||
const subCategory = parseCategory(data as CategoryData, context);
|
if (isCategory(data)) {
|
||||||
children.subCategories.push(subCategory);
|
const subCategory = parseCategory(data as CategoryData, context);
|
||||||
} else if (isScript(data)) {
|
children.subCategories.push(subCategory);
|
||||||
const scriptData = data as ScriptData;
|
} else if (isScript(data)) {
|
||||||
const script = parseScript(scriptData, context);
|
const scriptData = data as ScriptData;
|
||||||
children.subScripts.push(script);
|
const script = parseScript(scriptData, context);
|
||||||
} else {
|
children.subScripts.push(script);
|
||||||
throw new Error(`Child element is neither a category or a script.
|
} else {
|
||||||
|
throw new Error(`Child element is neither a category or a script.
|
||||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isScript(data: any): boolean {
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
return (data.code && data.code.length > 0)
|
const holder = (data as InstructionHolder);
|
||||||
|| data.call;
|
return hasCode(holder) || hasCall(holder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: any): boolean {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
return data.category && data.category.length > 0;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,64 @@
|
|||||||
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
|
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
|
||||||
|
|
||||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||||
if (!documentable) {
|
if (!documentable) {
|
||||||
throw new Error('documentable is null or undefined');
|
throw new Error('documentable is null or undefined');
|
||||||
}
|
}
|
||||||
const docs = documentable.docs;
|
const { docs } = documentable;
|
||||||
if (!docs || !docs.length) {
|
if (!docs || !docs.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let result = new DocumentationUrlContainer();
|
let result = new DocumentationUrlContainer();
|
||||||
result = addDocs(docs, result);
|
result = addDocs(docs, result);
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
function addDocs(
|
||||||
if (docs instanceof Array) {
|
docs: DocumentationUrlsData,
|
||||||
urls.addUrls(docs);
|
urls: DocumentationUrlContainer,
|
||||||
} else if (typeof docs === 'string') {
|
): DocumentationUrlContainer {
|
||||||
urls.addUrl(docs);
|
if (docs instanceof Array) {
|
||||||
} else {
|
urls.addUrls(docs);
|
||||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
} else if (typeof docs === 'string') {
|
||||||
}
|
urls.addUrl(docs);
|
||||||
return urls;
|
} else {
|
||||||
|
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentationUrlContainer {
|
class DocumentationUrlContainer {
|
||||||
private readonly urls = new Array<string>();
|
private readonly urls = new Array<string>();
|
||||||
|
|
||||||
public addUrl(url: string) {
|
public addUrl(url: string) {
|
||||||
validateUrl(url);
|
validateUrl(url);
|
||||||
this.urls.push(url);
|
this.urls.push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUrls(urls: readonly any[]) {
|
public addUrls(urls: readonly string[]) {
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (typeof url !== 'string') {
|
if (typeof url !== 'string') {
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||||
}
|
}
|
||||||
this.addUrl(url);
|
this.addUrl(url);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getAll(): ReadonlyArray<string> {
|
public getAll(): ReadonlyArray<string> {
|
||||||
return this.urls;
|
return this.urls;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUrl(docUrl: string): void {
|
function validateUrl(docUrl: string): void {
|
||||||
if (!docUrl) {
|
if (!docUrl) {
|
||||||
throw new Error('Documentation url is null or empty');
|
throw new Error('Documentation url is null or empty');
|
||||||
}
|
}
|
||||||
if (docUrl.includes('\n')) {
|
if (docUrl.includes('\n')) {
|
||||||
throw new Error('Documentation url cannot be multi-lined.');
|
throw new Error('Documentation url cannot be multi-lined.');
|
||||||
}
|
}
|
||||||
const res = docUrl.match(
|
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||||
/(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) {
|
if (res == null) {
|
||||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
|||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
|
||||||
export function parseProjectInformation(
|
export function parseProjectInformation(
|
||||||
environment: NodeJS.ProcessEnv): IProjectInformation {
|
environment: NodeJS.ProcessEnv,
|
||||||
return new ProjectInformation(
|
): IProjectInformation {
|
||||||
environment.VUE_APP_NAME,
|
return new ProjectInformation(
|
||||||
environment.VUE_APP_VERSION,
|
environment.VUE_APP_NAME,
|
||||||
environment.VUE_APP_REPOSITORY_URL,
|
environment.VUE_APP_VERSION,
|
||||||
environment.VUE_APP_HOMEPAGE_URL,
|
environment.VUE_APP_REPOSITORY_URL,
|
||||||
);
|
environment.VUE_APP_HOMEPAGE_URL,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
@@ -8,15 +8,17 @@ import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
|||||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
public readonly compiler: IScriptCompiler;
|
public readonly compiler: IScriptCompiler;
|
||||||
public readonly syntax: ILanguageSyntax;
|
|
||||||
|
|
||||||
constructor(
|
public readonly syntax: ILanguageSyntax;
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
|
||||||
scripting: IScriptingDefinition,
|
constructor(
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
if (!scripting) { throw new Error('undefined scripting'); }
|
scripting: IScriptingDefinition,
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
) {
|
||||||
}
|
if (!scripting) { throw new Error('undefined scripting'); }
|
||||||
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
|
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,63 @@
|
|||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { IExpression } from './IExpression';
|
|
||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
|
||||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import { IExpression } from './IExpression';
|
||||||
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly position: ExpressionPosition,
|
public readonly position: ExpressionPosition,
|
||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
|
public readonly parameters
|
||||||
if (!position) {
|
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
|
||||||
throw new Error('undefined position');
|
) {
|
||||||
}
|
if (!position) {
|
||||||
if (!evaluator) {
|
throw new Error('undefined position');
|
||||||
throw new Error('undefined evaluator');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
if (!evaluator) {
|
||||||
if (!context) {
|
throw new Error('undefined evaluator');
|
||||||
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(
|
function validateThatAllRequiredParametersAreSatisfied(
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
) {
|
) {
|
||||||
const requiredParameterNames = parameters
|
const requiredParameterNames = parameters
|
||||||
.all
|
.all
|
||||||
.filter((parameter) => !parameter.isOptional)
|
.filter((parameter) => !parameter.isOptional)
|
||||||
.map((parameter) => parameter.name);
|
.map((parameter) => parameter.name);
|
||||||
const missingParameterNames = requiredParameterNames
|
const missingParameterNames = requiredParameterNames
|
||||||
.filter((parameterName) => !args.hasArgument(parameterName));
|
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||||
if (missingParameterNames.length) {
|
if (missingParameterNames.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
|
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterUnusedArguments(
|
function filterUnusedArguments(
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
|
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
for (const parameter of parameters.all) {
|
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||||
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
|
parameters.all
|
||||||
continue; // Optional parameter is not necessarily provided
|
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
|
||||||
}
|
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
|
||||||
const arg = allFunctionArgs.getArgument(parameter.name);
|
.forEach((argument) => specificCallArgs.addArgument(argument));
|
||||||
specificCallArgs.addArgument(arg);
|
return specificCallArgs;
|
||||||
}
|
|
||||||
return specificCallArgs;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
|||||||
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
||||||
|
|
||||||
export interface IExpressionEvaluationContext {
|
export interface IExpressionEvaluationContext {
|
||||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
readonly pipelineCompiler: IPipelineCompiler;
|
readonly pipelineCompiler: IPipelineCompiler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
if (!args) {
|
) {
|
||||||
throw new Error('undefined args, send empty collection instead');
|
if (!args) {
|
||||||
}
|
throw new Error('undefined args, send empty collection instead');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
export class ExpressionPosition {
|
export class ExpressionPosition {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly start: number,
|
public readonly start: number,
|
||||||
public readonly end: number) {
|
public readonly end: number,
|
||||||
if (start === end) {
|
) {
|
||||||
throw new Error(`no length (start = end = ${start})`);
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ExpressionPosition } from './ExpressionPosition';
|
|
||||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
export interface IExpression {
|
export interface IExpression {
|
||||||
readonly position: ExpressionPosition;
|
readonly position: ExpressionPosition;
|
||||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
evaluate(context: IExpressionEvaluationContext): string;
|
evaluate(context: IExpressionEvaluationContext): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,85 @@
|
|||||||
|
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||||
import { IExpression } from './Expression/IExpression';
|
import { IExpression } from './Expression/IExpression';
|
||||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
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 {
|
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
|
||||||
public compileExpressions(
|
) { }
|
||||||
code: string,
|
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string {
|
public compileExpressions(
|
||||||
if (!args) {
|
code: string | undefined,
|
||||||
throw new Error('undefined args, send empty collection instead');
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
}
|
): string {
|
||||||
const expressions = this.extractor.findExpressions(code);
|
if (!args) {
|
||||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
throw new Error('undefined args, send empty collection instead');
|
||||||
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(
|
function compileExpressions(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
code: string,
|
code: string,
|
||||||
context: IExpressionEvaluationContext) {
|
context: IExpressionEvaluationContext,
|
||||||
let compiledCode = '';
|
) {
|
||||||
const sortedExpressions = expressions
|
let compiledCode = '';
|
||||||
.slice() // copy the array to not mutate the parameter
|
const sortedExpressions = expressions
|
||||||
.sort((a, b) => b.position.start - a.position.start);
|
.slice() // copy the array to not mutate the parameter
|
||||||
let index = 0;
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
while (index !== code.length) {
|
let index = 0;
|
||||||
const nextExpression = sortedExpressions.pop();
|
while (index !== code.length) {
|
||||||
if (nextExpression) {
|
const nextExpression = sortedExpressions.pop();
|
||||||
compiledCode += code.substring(index, nextExpression.position.start);
|
if (nextExpression) {
|
||||||
const expressionCode = nextExpression.evaluate(context);
|
compiledCode += code.substring(index, nextExpression.position.start);
|
||||||
compiledCode += expressionCode;
|
const expressionCode = nextExpression.evaluate(context);
|
||||||
index = nextExpression.position.end;
|
compiledCode += expressionCode;
|
||||||
} else {
|
index = nextExpression.position.end;
|
||||||
compiledCode += code.substring(index, code.length);
|
} else {
|
||||||
break;
|
compiledCode += code.substring(index, code.length);
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
return compiledCode;
|
}
|
||||||
|
return compiledCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractRequiredParameterNames(
|
function extractRequiredParameterNames(
|
||||||
expressions: readonly IExpression[]): string[] {
|
expressions: readonly IExpression[],
|
||||||
const usedParameterNames = expressions
|
): string[] {
|
||||||
.map((e) => e.parameters.all
|
return expressions
|
||||||
.filter((p) => !p.isOptional)
|
.map((e) => e.parameters.all
|
||||||
.map((p) => p.name))
|
.filter((p) => !p.isOptional)
|
||||||
.filter((p) => p)
|
.map((p) => p.name))
|
||||||
.flat();
|
.filter(Boolean) // Remove empty or undefined
|
||||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
.flat()
|
||||||
return uniqueParameterNames;
|
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureParamsUsedInCodeHasArgsProvided(
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
): void {
|
||||||
if (!usedParameterNames?.length) {
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
return;
|
if (!usedParameterNames?.length) {
|
||||||
}
|
return;
|
||||||
const notProvidedParameters = usedParameterNames
|
}
|
||||||
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
const notProvidedParameters = usedParameterNames
|
||||||
if (notProvidedParameters.length) {
|
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
if (notProvidedParameters.length) {
|
||||||
}
|
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
function printList(list: readonly string[]): string {
|
||||||
return `"${list.join('", "')}"`;
|
return `"${list.join('", "')}"`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
code: string,
|
code: string | undefined,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
import { IExpressionParser } from './IExpressionParser';
|
|
||||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||||
import { WithParser } from '../SyntaxParsers/WithParser';
|
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||||
|
import { IExpressionParser } from './IExpressionParser';
|
||||||
|
|
||||||
const Parsers = [
|
const Parsers = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
new WithParser(),
|
new WithParser(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
if (leafs.some((leaf) => !leaf)) {
|
if (leafs.some((leaf) => !leaf)) {
|
||||||
throw new Error('undefined 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) || [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
|
||||||
export interface IExpressionParser {
|
export interface IExpressionParser {
|
||||||
findExpressions(code: string): IExpression[];
|
findExpressions(code: string): IExpression[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,60 @@
|
|||||||
export class ExpressionRegexBuilder {
|
export class ExpressionRegexBuilder {
|
||||||
private readonly parts = new Array<string>();
|
private readonly parts = new Array<string>();
|
||||||
|
|
||||||
public expectCharacters(characters: string) {
|
public expectCharacters(characters: string) {
|
||||||
return this.addRawRegex(
|
return this.addRawRegex(
|
||||||
characters
|
characters
|
||||||
.replaceAll('$', '\\$')
|
.replaceAll('$', '\\$')
|
||||||
.replaceAll('.', '\\.'),
|
.replaceAll('.', '\\.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectOneOrMoreWhitespaces() {
|
public expectOneOrMoreWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('\\s+');
|
.addRawRegex('\\s+');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchPipeline() {
|
public matchPipeline() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('(\\|\\s*.+?)?');
|
.addRawRegex('(\\|\\s*.+?)?');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchUntilFirstWhitespace() {
|
public matchUntilFirstWhitespace() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchAnythingExceptSurroundingWhitespaces() {
|
public matchAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('(.+?)')
|
.addRawRegex('(.+?)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionStart() {
|
public expectExpressionStart() {
|
||||||
return this
|
return this
|
||||||
.expectCharacters('{{')
|
.expectCharacters('{{')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionEnd() {
|
public expectExpressionEnd() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.expectCharacters('}}');
|
.expectCharacters('}}');
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildRegExp(): RegExp {
|
public buildRegExp(): RegExp {
|
||||||
return new RegExp(this.parts.join(''), 'g');
|
return new RegExp(this.parts.join(''), 'g');
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectZeroOrMoreWhitespaces() {
|
private expectZeroOrMoreWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('\\s*');
|
.addRawRegex('\\s*');
|
||||||
}
|
}
|
||||||
private addRawRegex(regex: string) {
|
|
||||||
this.parts.push(regex);
|
private addRawRegex(regex: string) {
|
||||||
return this;
|
this.parts.push(regex);
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,43 +6,54 @@ import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParamet
|
|||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
return Array.from(this.findRegexExpressions(code));
|
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');
|
||||||
}
|
}
|
||||||
|
const matches = code.matchAll(this.regex);
|
||||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
for (const match of matches) {
|
||||||
|
const primitiveExpression = this.buildExpression(match);
|
||||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
||||||
const matches = Array.from(code.matchAll(this.regex));
|
const parameters = createParameters(primitiveExpression);
|
||||||
for (const match of matches) {
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
const startPos = match.index;
|
yield expression;
|
||||||
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 {
|
export interface IPrimitiveExpression {
|
||||||
evaluator: ExpressionEvaluator;
|
evaluator: ExpressionEvaluator;
|
||||||
parameters?: readonly IFunctionParameter[];
|
parameters?: readonly IFunctionParameter[];
|
||||||
}
|
|
||||||
|
|
||||||
function getParameters(
|
|
||||||
expression: IPrimitiveExpression): FunctionParameterCollection {
|
|
||||||
const parameters = new FunctionParameterCollection();
|
|
||||||
for (const parameter of expression.parameters || []) {
|
|
||||||
parameters.addParameter(parameter);
|
|
||||||
}
|
|
||||||
return parameters;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IPipe {
|
export interface IPipe {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
apply(input: string): string;
|
apply(input: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface IPipelineCompiler {
|
export interface IPipelineCompiler {
|
||||||
compile(value: string, pipeline: string): string;
|
compile(value: string, pipeline: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
import { IPipe } from '../IPipe';
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class EscapeDoubleQuotes implements IPipe {
|
export class EscapeDoubleQuotes implements IPipe {
|
||||||
public readonly name: string = 'escapeDoubleQuotes';
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
public apply(raw: string): string {
|
|
||||||
return raw?.replaceAll('"', '"^""');
|
public apply(raw: string): string {
|
||||||
/*
|
return raw?.replaceAll('"', '"^""');
|
||||||
"^"" is the most robust and stable choice.
|
/* eslint-disable max-len */
|
||||||
Other options:
|
/*
|
||||||
""
|
"^"" is the most robust and stable choice.
|
||||||
Breaks, because it is fundamentally unsupported
|
Other options:
|
||||||
""""
|
""
|
||||||
Does not work with consecutive double quotes.
|
Breaks, because it is fundamentally unsupported
|
||||||
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
|
""""
|
||||||
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
|
Does not work with consecutive double quotes.
|
||||||
\"
|
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
|
||||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
|
||||||
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"^""'"
|
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 "&"
|
||||||
Normalizes interior whitespace
|
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
|
||||||
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
|
\""
|
||||||
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
|
Normalizes interior whitespace
|
||||||
A good explanation: https://stackoverflow.com/a/31413730
|
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 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,166 @@
|
|||||||
import { IPipe } from '../IPipe';
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class InlinePowerShell implements IPipe {
|
export class InlinePowerShell implements IPipe {
|
||||||
public readonly name: string = 'inlinePowerShell';
|
public readonly name: string = 'inlinePowerShell';
|
||||||
public apply(code: string): string {
|
|
||||||
if (!code || !hasLines(code)) {
|
public apply(code: string): string {
|
||||||
return code;
|
if (!code || !hasLines(code)) {
|
||||||
}
|
return code;
|
||||||
code = replaceComments(code);
|
|
||||||
code = mergeLinesWithBacktick(code);
|
|
||||||
code = mergeHereStrings(code);
|
|
||||||
const lines = getLines(code)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter((line) => line.length > 0);
|
|
||||||
return lines
|
|
||||||
.join('; ');
|
|
||||||
}
|
}
|
||||||
|
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) {
|
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.. #>
|
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||||
Otherwise single # comments out rest of the code
|
Otherwise single # comments out rest of the code
|
||||||
*/
|
*/
|
||||||
function replaceComments(code: string) {
|
function inlineComments(code: string): string {
|
||||||
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (_$, match1 ) => {
|
const makeInlineComment = (comment: string) => {
|
||||||
const value = match1?.trim();
|
const value = comment?.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '<##>';
|
return '<##>';
|
||||||
}
|
}
|
||||||
return `<# ${value} #>`;
|
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) {
|
||||||
|
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) {
|
function getLines(code: string): string[] {
|
||||||
return (code.split(/\r\n|\r|\n/) || []);
|
return (code?.split(/\r\n|\r|\n/) || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||||
*/
|
*/
|
||||||
function mergeHereStrings(code: string) {
|
function mergeHereStrings(code: string) {
|
||||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||||
const newString = getHereStringHandler(quotes);
|
const newString = getHereStringHandler(quotes);
|
||||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||||
const lines = getLines(escaped);
|
const lines = getLines(escaped);
|
||||||
const inlined = lines.join(newString.separator);
|
const inlined = lines.join(newString.separator);
|
||||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||||
return quoted;
|
return quoted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
interface IInlinedHereString {
|
interface IInlinedHereString {
|
||||||
readonly quotesAround: string;
|
readonly quotesAround: string;
|
||||||
readonly escapedQuotes: string;
|
readonly escapedQuotes: string;
|
||||||
readonly separator: string;
|
readonly separator: string;
|
||||||
}
|
}
|
||||||
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
|
|
||||||
function getHereStringHandler(quotes: string): IInlinedHereString {
|
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||||
const expandableNewLine = '`r`n';
|
/*
|
||||||
switch (quotes) {
|
We handle @' and @" differently.
|
||||||
case '\'':
|
Single quotes are interpreted literally and doubles are expandable.
|
||||||
return {
|
*/
|
||||||
quotesAround: '\'',
|
const expandableNewLine = '`r`n';
|
||||||
escapedQuotes: '\'\'',
|
switch (quotes) {
|
||||||
separator: `\'+"${expandableNewLine}"+\'`,
|
case '\'':
|
||||||
};
|
return {
|
||||||
case '"':
|
quotesAround: '\'',
|
||||||
return {
|
escapedQuotes: '\'\'',
|
||||||
quotesAround: '"',
|
separator: `'+"${expandableNewLine}"+'`,
|
||||||
escapedQuotes: '`"',
|
};
|
||||||
separator: expandableNewLine,
|
case '"':
|
||||||
};
|
return {
|
||||||
default:
|
quotesAround: '"',
|
||||||
throw new Error(`expected quotes: ${quotes}`);
|
escapedQuotes: '`"',
|
||||||
}
|
separator: expandableNewLine,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`expected quotes: ${quotes}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Input ->
|
Input ->
|
||||||
Get-Service * `
|
Get-Service * `
|
||||||
Sort-Object StartType `
|
Sort-Object StartType `
|
||||||
Format-Table Name, ServiceType, Status -AutoSize
|
Format-Table Name, ServiceType, Status -AutoSize
|
||||||
Output ->
|
Output ->
|
||||||
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||||
*/
|
*/
|
||||||
function mergeLinesWithBacktick(code: string) {
|
function mergeLinesWithBacktick(code: string) {
|
||||||
/*
|
/*
|
||||||
The regex actually wraps any whitespace character after backtick and before newline
|
The regex actually wraps any whitespace character after backtick and before newline
|
||||||
However, this is not always the case for PowerShell.
|
However, this is not always the case for PowerShell.
|
||||||
I see two behaviors:
|
I see two behaviors:
|
||||||
1. If inside string, it's accepted (inside " or ')
|
1. If inside string, it's accepted (inside " or ')
|
||||||
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
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
|
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||||
we wrap it anyway
|
we wrap it anyway
|
||||||
*/
|
*/
|
||||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
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('; ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,45 +3,48 @@ import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
|||||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
|
|
||||||
const RegisteredPipes = [
|
const RegisteredPipes = [
|
||||||
new EscapeDoubleQuotes(),
|
new EscapeDoubleQuotes(),
|
||||||
new InlinePowerShell(),
|
new InlinePowerShell(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface IPipeFactory {
|
export interface IPipeFactory {
|
||||||
get(pipeName: string): IPipe;
|
get(pipeName: string): IPipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PipeFactory implements IPipeFactory {
|
export class PipeFactory implements IPipeFactory {
|
||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
throw new Error('undefined pipe in list');
|
if (pipes.some((pipe) => !pipe)) {
|
||||||
}
|
throw new Error('undefined pipe in list');
|
||||||
for (const pipe of pipes) {
|
|
||||||
this.registerPipe(pipe);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public get(pipeName: string): IPipe {
|
for (const pipe of pipes) {
|
||||||
validatePipeName(pipeName);
|
this.registerPipe(pipe);
|
||||||
if (!this.pipes.has(pipeName)) {
|
|
||||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
|
||||||
}
|
|
||||||
return this.pipes.get(pipeName);
|
|
||||||
}
|
}
|
||||||
private registerPipe(pipe: IPipe): void {
|
}
|
||||||
validatePipeName(pipe.name);
|
|
||||||
if (this.pipes.has(pipe.name)) {
|
public get(pipeName: string): IPipe {
|
||||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
validatePipeName(pipeName);
|
||||||
}
|
if (!this.pipes.has(pipeName)) {
|
||||||
this.pipes.set(pipe.name, pipe);
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
}
|
}
|
||||||
|
return this.pipes.get(pipeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
validatePipeName(pipe.name);
|
||||||
|
if (this.pipes.has(pipe.name)) {
|
||||||
|
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||||
|
}
|
||||||
|
this.pipes.set(pipe.name, pipe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validatePipeName(name: string) {
|
function validatePipeName(name: string) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('empty pipe name');
|
throw new Error('empty pipe name');
|
||||||
}
|
}
|
||||||
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||||
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,30 @@ import { IPipeFactory, PipeFactory } from './PipeFactory';
|
|||||||
import { IPipelineCompiler } from './IPipelineCompiler';
|
import { IPipelineCompiler } from './IPipelineCompiler';
|
||||||
|
|
||||||
export class PipelineCompiler implements IPipelineCompiler {
|
export class PipelineCompiler implements IPipelineCompiler {
|
||||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||||
public compile(value: string, pipeline: string): string {
|
|
||||||
ensureValidArguments(value, pipeline);
|
public compile(value: string, pipeline: string): string {
|
||||||
const pipeNames = extractPipeNames(pipeline);
|
ensureValidArguments(value, pipeline);
|
||||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
const pipeNames = extractPipeNames(pipeline);
|
||||||
for (const pipe of pipes) {
|
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||||
value = pipe.apply(value);
|
return pipes.reduce((previousValue, pipe) => {
|
||||||
}
|
return pipe.apply(previousValue);
|
||||||
return value;
|
}, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPipeNames(pipeline: string): string[] {
|
function extractPipeNames(pipeline: string): string[] {
|
||||||
return pipeline
|
return pipeline
|
||||||
.trim()
|
.trim()
|
||||||
.split('|')
|
.split('|')
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((p) => p.trim());
|
.map((p) => p.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidArguments(value: string, pipeline: string) {
|
function ensureValidArguments(value: string, pipeline: string) {
|
||||||
if (!value) { throw new Error('undefined value'); }
|
if (!value) { throw new Error('undefined value'); }
|
||||||
if (!pipeline) { throw new Error('undefined pipeline'); }
|
if (!pipeline) { throw new Error('undefined pipeline'); }
|
||||||
if (!pipeline.trimStart().startsWith('|')) {
|
if (!pipeline.trimStart().startsWith('|')) {
|
||||||
throw new Error('pipeline does not start with pipe');
|
throw new Error('pipeline does not start with pipe');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class ParameterSubstitutionParser extends RegexParser {
|
export class ParameterSubstitutionParser extends RegexParser {
|
||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('$')
|
.expectCharacters('$')
|
||||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||||
.matchPipeline() // Second match: Pipeline
|
.matchPipeline() // Second match: Pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
const parameterName = match[1];
|
const parameterName = match[1];
|
||||||
const pipeline = match[2];
|
const pipeline = match[2];
|
||||||
return {
|
return {
|
||||||
parameters: [ new FunctionParameter(parameterName, false) ],
|
parameters: [new FunctionParameter(parameterName, false)],
|
||||||
evaluator: (context) => {
|
evaluator: (context) => {
|
||||||
const argumentValue = context.args.getArgument(parameterName).argumentValue;
|
const { argumentValue } = context.args.getArgument(parameterName);
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
return argumentValue;
|
return argumentValue;
|
||||||
}
|
}
|
||||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,59 @@
|
|||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class WithParser extends RegexParser {
|
export class WithParser extends RegexParser {
|
||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
// {{ with $parameterName }}
|
// {{ with $parameterName }}
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('with')
|
|
||||||
.expectOneOrMoreWhitespaces()
|
|
||||||
.expectCharacters('$')
|
|
||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
|
||||||
.expectExpressionEnd()
|
|
||||||
// ...
|
|
||||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
|
||||||
// {{ end }}
|
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('end')
|
|
||||||
.expectExpressionEnd()
|
|
||||||
.buildRegExp();
|
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
|
||||||
const parameterName = match[1];
|
|
||||||
const scopeText = match[2];
|
|
||||||
return {
|
|
||||||
parameters: [ new FunctionParameter(parameterName, true) ],
|
|
||||||
evaluator: (context) => {
|
|
||||||
const argumentValue = context.args.hasArgument(parameterName) ?
|
|
||||||
context.args.getArgument(parameterName).argumentValue
|
|
||||||
: undefined;
|
|
||||||
if (!argumentValue) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
|
||||||
if (!pipeline) {
|
|
||||||
return argumentValue;
|
|
||||||
}
|
|
||||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
|
||||||
// {{ . | pipeName }}
|
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('.')
|
.expectCharacters('with')
|
||||||
.matchPipeline() // First match: pipeline
|
.expectOneOrMoreWhitespaces()
|
||||||
|
.expectCharacters('$')
|
||||||
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
// ...
|
||||||
|
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
|
// {{ end }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
|
const parameterName = match[1];
|
||||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
|
const scopeText = match[2];
|
||||||
return replacer(match1);
|
return {
|
||||||
});
|
parameters: [new FunctionParameter(parameterName, true)],
|
||||||
|
evaluator: (context) => {
|
||||||
|
const argumentValue = context.args.hasArgument(parameterName)
|
||||||
|
? context.args.getArgument(parameterName).argumentValue
|
||||||
|
: undefined;
|
||||||
|
if (!argumentValue) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||||
|
if (!pipeline) {
|
||||||
|
return argumentValue;
|
||||||
|
}
|
||||||
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||||
|
// {{ . | pipeName }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('.')
|
||||||
|
.matchPipeline() // First match: pipeline
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||||
|
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||||
|
// but instead letting the pipeline compiler to fail on those.
|
||||||
|
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||||
|
return replacer(match1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
|
||||||
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
export class FunctionCallArgument implements IFunctionCallArgument {
|
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly parameterName: string,
|
public readonly parameterName: string,
|
||||||
public readonly argumentValue: string) {
|
public readonly argumentValue: string,
|
||||||
ensureValidParameterName(parameterName);
|
) {
|
||||||
if (!argumentValue) {
|
ensureValidParameterName(parameterName);
|
||||||
throw new Error(`undefined argument value for "${parameterName}"`);
|
if (!argumentValue) {
|
||||||
}
|
throw new Error(`undefined argument value for "${parameterName}"`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,37 @@ import { IFunctionCallArgument } from './IFunctionCallArgument';
|
|||||||
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
public addArgument(argument: IFunctionCallArgument): void {
|
|
||||||
if (!argument) {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
throw new Error('undefined argument');
|
if (!argument) {
|
||||||
}
|
throw new Error('undefined argument');
|
||||||
if (this.hasArgument(argument.parameterName)) {
|
|
||||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
|
||||||
}
|
|
||||||
this.arguments.set(argument.parameterName, argument);
|
|
||||||
}
|
}
|
||||||
public getAllParameterNames(): string[] {
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
return Array.from(this.arguments.keys());
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
}
|
}
|
||||||
public hasArgument(parameterName: string): boolean {
|
this.arguments.set(argument.parameterName, argument);
|
||||||
if (!parameterName) {
|
}
|
||||||
throw new Error('undefined parameter name');
|
|
||||||
}
|
public getAllParameterNames(): string[] {
|
||||||
return this.arguments.has(parameterName);
|
return Array.from(this.arguments.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasArgument(parameterName: string): boolean {
|
||||||
|
if (!parameterName) {
|
||||||
|
throw new Error('undefined parameter name');
|
||||||
}
|
}
|
||||||
public getArgument(parameterName: string): IFunctionCallArgument {
|
return this.arguments.has(parameterName);
|
||||||
if (!parameterName) {
|
}
|
||||||
throw new Error('undefined parameter name');
|
|
||||||
}
|
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||||
const arg = this.arguments.get(parameterName);
|
if (!parameterName) {
|
||||||
if (!arg) {
|
throw new Error('undefined parameter name');
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IFunctionCallArgument {
|
export interface IFunctionCallArgument {
|
||||||
readonly parameterName: string;
|
readonly parameterName: string;
|
||||||
readonly argumentValue: string;
|
readonly argumentValue: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
export interface IReadOnlyFunctionCallArgumentCollection {
|
export interface IReadOnlyFunctionCallArgumentCollection {
|
||||||
getArgument(parameterName: string): IFunctionCallArgument;
|
getArgument(parameterName: string): IFunctionCallArgument;
|
||||||
getAllParameterNames(): string[];
|
getAllParameterNames(): string[];
|
||||||
hasArgument(parameterName: string): boolean;
|
hasArgument(parameterName: string): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
||||||
addArgument(argument: IFunctionCallArgument): void;
|
addArgument(argument: IFunctionCallArgument): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +1,149 @@
|
|||||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import { ICompiledCode } from './ICompiledCode';
|
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||||
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
|
||||||
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||||
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||||
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
|
||||||
import { FunctionCall } from '../FunctionCall';
|
import { FunctionCall } from '../FunctionCall';
|
||||||
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
|
||||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) {
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||||
}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public compileCall(
|
public compileCall(
|
||||||
calls: IFunctionCall[],
|
calls: IFunctionCall[],
|
||||||
functions: ISharedFunctionCollection): ICompiledCode {
|
functions: ISharedFunctionCollection,
|
||||||
if (!functions) { throw new Error('undefined functions'); }
|
): ICompiledCode {
|
||||||
if (!calls) { throw new Error('undefined calls'); }
|
if (!functions) { throw new Error('undefined functions'); }
|
||||||
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
if (!calls) { throw new Error('undefined calls'); }
|
||||||
const context: ICompilationContext = {
|
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
||||||
allFunctions: functions,
|
const context: ICompilationContext = {
|
||||||
callSequence: calls,
|
allFunctions: functions,
|
||||||
expressionsCompiler: this.expressionsCompiler,
|
callSequence: calls,
|
||||||
};
|
expressionsCompiler: this.expressionsCompiler,
|
||||||
const code = compileCallSequence(context);
|
};
|
||||||
return code;
|
const code = compileCallSequence(context);
|
||||||
}
|
return code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICompilationContext {
|
interface ICompilationContext {
|
||||||
allFunctions: ISharedFunctionCollection;
|
allFunctions: ISharedFunctionCollection;
|
||||||
callSequence: readonly IFunctionCall[];
|
callSequence: readonly IFunctionCall[];
|
||||||
expressionsCompiler: IExpressionsCompiler;
|
expressionsCompiler: IExpressionsCompiler;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICompiledFunctionCall {
|
interface ICompiledFunctionCall {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
readonly revertCode: string;
|
readonly revertCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||||
const compiledFunctions = new Array<ICompiledFunctionCall>();
|
const compiledFunctions = context.callSequence
|
||||||
for (const call of context.callSequence) {
|
.flatMap((call) => compileSingleCall(call, context));
|
||||||
const compiledCode = compileSingleCall(call, context);
|
return {
|
||||||
compiledFunctions.push(...compiledCode);
|
code: merge(compiledFunctions.map((f) => f.code)),
|
||||||
}
|
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||||
return {
|
};
|
||||||
code: merge(compiledFunctions.map((f) => f.code)),
|
|
||||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] {
|
function compileSingleCall(
|
||||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
call: IFunctionCall,
|
||||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
context: ICompilationContext,
|
||||||
if (func.body.code) { // Function with inline code
|
): ICompiledFunctionCall[] {
|
||||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||||
return [ compiledCode ];
|
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||||
} else { // Function with inner calls
|
if (func.body.code) { // Function with inline code
|
||||||
return func.body.calls
|
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||||
.map((innerCall) => {
|
return [compiledCode];
|
||||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
}
|
||||||
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
// Function with inner calls
|
||||||
return compileSingleCall(compiledCall, context);
|
return func.body.calls
|
||||||
})
|
.map((innerCall) => {
|
||||||
.flat();
|
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||||
}
|
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||||
|
return compileSingleCall(compiledCall, context);
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileCode(
|
function compileCode(
|
||||||
code: IFunctionCode,
|
code: IFunctionCode,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
compiler: IExpressionsCompiler): ICompiledFunctionCall {
|
compiler: IExpressionsCompiler,
|
||||||
return {
|
): ICompiledFunctionCall {
|
||||||
code: compiler.compileExpressions(code.do, args),
|
return {
|
||||||
revertCode: compiler.compileExpressions(code.revert, args),
|
code: compiler.compileExpressions(code.do, args),
|
||||||
};
|
revertCode: compiler.compileExpressions(code.revert, args),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileArgs(
|
function compileArgs(
|
||||||
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
compiler: IExpressionsCompiler,
|
compiler: IExpressionsCompiler,
|
||||||
): IReadOnlyFunctionCallArgumentCollection {
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
const compiledArgs = new FunctionCallArgumentCollection();
|
return argsToCompile
|
||||||
for (const parameterName of argsToCompile.getAllParameterNames()) {
|
.getAllParameterNames()
|
||||||
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue;
|
.map((parameterName) => {
|
||||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||||
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
|
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||||
compiledArgs.addArgument(newArgument);
|
return new FunctionCallArgument(parameterName, compiledValue);
|
||||||
}
|
})
|
||||||
return compiledArgs;
|
.reduce((compiledArgs, arg) => {
|
||||||
|
compiledArgs.addArgument(arg);
|
||||||
|
return compiledArgs;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
function merge(codeParts: readonly string[]): string {
|
function merge(codeParts: readonly string[]): string {
|
||||||
return codeParts
|
return codeParts
|
||||||
.filter((part) => part?.length > 0)
|
.filter((part) => part?.length > 0)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||||
func: ISharedFunction,
|
func: ISharedFunction,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): void {
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
const callArgumentNames = args.getAllParameterNames();
|
): void {
|
||||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
const callArgumentNames = args.getAllParameterNames();
|
||||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||||
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||||
|
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findUnexpectedParameters(
|
function findUnexpectedParameters(
|
||||||
callArgumentNames: string[],
|
callArgumentNames: string[],
|
||||||
functionParameterNames: string[]): string[] {
|
functionParameterNames: string[],
|
||||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
): string[] {
|
||||||
return [];
|
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||||
}
|
return [];
|
||||||
return callArgumentNames
|
}
|
||||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
return callArgumentNames
|
||||||
|
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||||
}
|
}
|
||||||
|
|
||||||
function throwIfNotEmpty(
|
function throwIfNotEmpty(
|
||||||
functionName: string,
|
functionName: string,
|
||||||
unexpectedParameters: string[],
|
unexpectedParameters: string[],
|
||||||
expectedParameters: string[]) {
|
expectedParameters: string[],
|
||||||
if (!unexpectedParameters.length) {
|
) {
|
||||||
return;
|
if (!unexpectedParameters.length) {
|
||||||
}
|
return;
|
||||||
throw new Error(
|
}
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: ` +
|
throw new Error(
|
||||||
`"${unexpectedParameters.join('", "')}"` +
|
// eslint-disable-next-line prefer-template
|
||||||
'. Expected parameter(s): ' +
|
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||||
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
+ `"${unexpectedParameters.join('", "')}"`
|
||||||
);
|
+ '. Expected parameter(s): '
|
||||||
|
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface ICompiledCode {
|
export interface ICompiledCode {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
readonly revertCode?: string;
|
readonly revertCode?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ICompiledCode } from './ICompiledCode';
|
|
||||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
import { IFunctionCall } from '../IFunctionCall';
|
import { IFunctionCall } from '../IFunctionCall';
|
||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
|
||||||
export interface IFunctionCallCompiler {
|
export interface IFunctionCallCompiler {
|
||||||
compileCall(
|
compileCall(
|
||||||
calls: IFunctionCall[],
|
calls: IFunctionCall[],
|
||||||
functions: ISharedFunctionCollection): ICompiledCode;
|
functions: ISharedFunctionCollection): ICompiledCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCal
|
|||||||
import { IFunctionCall } from './IFunctionCall';
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
|
|
||||||
export class FunctionCall implements IFunctionCall {
|
export class FunctionCall implements IFunctionCall {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly functionName: string,
|
public readonly functionName: string,
|
||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection) {
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
if (!functionName) {
|
) {
|
||||||
throw new Error('empty function name in function call');
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,42 @@
|
|||||||
import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*';
|
import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||||
import { IFunctionCall } from './IFunctionCall';
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||||
import { FunctionCall } from './FunctionCall';
|
import { FunctionCall } from './FunctionCall';
|
||||||
|
|
||||||
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||||
if (!calls) {
|
if (calls === undefined) {
|
||||||
throw new Error('undefined call data');
|
throw new Error('undefined call data');
|
||||||
}
|
}
|
||||||
const sequence = getCallSequence(calls);
|
const sequence = getCallSequence(calls);
|
||||||
return sequence.map((call) => parseFunctionCall(call));
|
return sequence.map((call) => parseFunctionCall(call));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||||
if (typeof calls !== 'object') {
|
if (typeof calls !== 'object') {
|
||||||
throw new Error('called function(s) must be an object');
|
throw new Error('called function(s) must be an object');
|
||||||
}
|
}
|
||||||
if (calls instanceof Array) {
|
if (calls instanceof Array) {
|
||||||
return calls as FunctionCallData[];
|
return calls as FunctionCallData[];
|
||||||
}
|
}
|
||||||
return [ calls as FunctionCallData ];
|
return [calls as FunctionCallData];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||||
if (!call) {
|
if (!call) {
|
||||||
throw new Error(`undefined function call`);
|
throw new Error('undefined function call');
|
||||||
}
|
}
|
||||||
const args = new FunctionCallArgumentCollection();
|
const callArgs = parseArgs(call.parameters);
|
||||||
for (const parameterName of Object.keys(call.parameters || {})) {
|
return new FunctionCall(call.function, callArgs);
|
||||||
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
|
}
|
||||||
args.addArgument(arg);
|
|
||||||
}
|
function parseArgs(
|
||||||
return new FunctionCall(call.function, args);
|
parameters: FunctionCallParametersData,
|
||||||
|
): FunctionCallArgumentCollection {
|
||||||
|
return Object.keys(parameters || {})
|
||||||
|
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
|
||||||
|
.reduce((args, arg) => {
|
||||||
|
args.addArgument(arg);
|
||||||
|
return args;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export interface IFunctionCall {
|
export interface IFunctionCall {
|
||||||
readonly functionName: string;
|
readonly functionName: string;
|
||||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
import { IFunctionCall } from './Call/IFunctionCall';
|
||||||
|
|
||||||
export interface ISharedFunction {
|
export interface ISharedFunction {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
readonly body: ISharedFunctionBody;
|
readonly body: ISharedFunctionBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISharedFunctionBody {
|
export interface ISharedFunctionBody {
|
||||||
readonly type: FunctionBodyType;
|
readonly type: FunctionBodyType;
|
||||||
readonly code: IFunctionCode;
|
readonly code: IFunctionCode;
|
||||||
readonly calls: readonly IFunctionCall[];
|
readonly calls: readonly IFunctionCall[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FunctionBodyType {
|
export enum FunctionBodyType {
|
||||||
Code,
|
Code,
|
||||||
Calls,
|
Calls,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFunctionCode {
|
export interface IFunctionCode {
|
||||||
readonly do: string;
|
readonly do: string;
|
||||||
readonly revert?: string;
|
readonly revert?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ISharedFunction } from './ISharedFunction';
|
import { ISharedFunction } from './ISharedFunction';
|
||||||
|
|
||||||
export interface ISharedFunctionCollection {
|
export interface ISharedFunctionCollection {
|
||||||
getFunctionByName(name: string): ISharedFunction;
|
getFunctionByName(name: string): ISharedFunction;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { FunctionData } from 'js-yaml-loader!@/*';
|
|||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
export interface ISharedFunctionsParser {
|
export interface ISharedFunctionsParser {
|
||||||
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user