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 |
58671
package-lock.json
generated
58671
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: {},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ 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>;
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
|
|
||||||
protected constructor(costlyGetter: ApplicationGetter) {
|
protected constructor(costlyGetter: ApplicationGetter) {
|
||||||
if (!costlyGetter) {
|
if (!costlyGetter) {
|
||||||
throw new Error('undefined getter');
|
throw new Error('undefined getter');
|
||||||
}
|
}
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
}
|
}
|
||||||
public getAppAsync(): Promise<IApplication> {
|
|
||||||
return this.getter.getValueAsync();
|
public getApp(): Promise<IApplication> {
|
||||||
|
return this.getter.getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
// 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>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): IEnumParser<TEnumValue> {
|
||||||
return {
|
return {
|
||||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
value: string,
|
value: string,
|
||||||
enumName: string,
|
enumName: string,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): TEnumValue {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new Error(`undefined ${enumName}`);
|
throw new Error(`undefined ${enumName}`);
|
||||||
}
|
}
|
||||||
@@ -29,22 +34,26 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
export function getEnumNames
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): string[] {
|
||||||
return Object
|
return Object
|
||||||
.values(enumVariable)
|
.values(enumVariable)
|
||||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
.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>,
|
||||||
|
): TEnumValue[] {
|
||||||
return getEnumNames(enumVariable)
|
return getEnumNames(enumVariable)
|
||||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>) {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
throw new Error('undefined enum value');
|
throw new Error('undefined enum value');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -27,5 +27,4 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
|||||||
}
|
}
|
||||||
this.getters.set(language, getter);
|
this.getters.set(language, getter);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
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 collection: ICategoryCollection;
|
||||||
|
|
||||||
public currentOs: OperatingSystem;
|
public currentOs: OperatingSystem;
|
||||||
|
|
||||||
public get state(): ICategoryCollectionState {
|
public get state(): ICategoryCollectionState {
|
||||||
@@ -19,9 +21,11 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly states: StateMachine;
|
private readonly states: StateMachine;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem) {
|
initialContext: OperatingSystem,
|
||||||
|
) {
|
||||||
validateApp(app);
|
validateApp(app);
|
||||||
assertInRange(initialContext, OperatingSystem);
|
assertInRange(initialContext, OperatingSystem);
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
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,
|
||||||
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('undefined factory'); }
|
if (!factory) { throw new Error('undefined factory'); }
|
||||||
if (!environment) { throw new Error('undefined environment'); }
|
if (!environment) { throw new Error('undefined environment'); }
|
||||||
const app = await factory.getAppAsync();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment);
|
const os = getInitialOs(app, environment);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
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: ICategoryCollectionState;
|
readonly state: IReadOnlyCategoryCollectionState;
|
||||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||||
|
readonly state: ICategoryCollectionState;
|
||||||
changeContext(os: OperatingSystem): void;
|
changeContext(os: OperatingSystem): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,13 +7,14 @@ 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 code: IApplicationCode;
|
||||||
|
|
||||||
public readonly selection: IUserSelection;
|
public readonly selection: IUserSelection;
|
||||||
|
|
||||||
public readonly filter: IUserFilter;
|
public readonly filter: IUserFilter;
|
||||||
|
|
||||||
public constructor(readonly collection: ICategoryCollection) {
|
public constructor(readonly collection: ICategoryCollection) {
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
|
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;
|
public current: string;
|
||||||
|
|
||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
userSelection: IUserSelection,
|
userSelection: IReadOnlyUserSelection,
|
||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
|
) {
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
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 addedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
private readonly scripts: Map<IScript, ICodePosition>;
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
@@ -14,7 +17,8 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
constructor(
|
constructor(
|
||||||
code: string,
|
code: string,
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
scripts: Map<SelectedScript, ICodePosition>) {
|
scripts: Map<SelectedScript, ICodePosition>,
|
||||||
|
) {
|
||||||
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||||
this.code = code;
|
this.code = code;
|
||||||
const newScripts = Array.from(scripts.keys());
|
const newScripts = Array.from(scripts.keys());
|
||||||
@@ -38,26 +42,29 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
|
|
||||||
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>,
|
||||||
|
): ReadonlyArray<IScript> {
|
||||||
return newScripts
|
return newScripts
|
||||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
&& oldScript.revert !== newScript.revert ))
|
&& oldScript.revert !== newScript.revert))
|
||||||
.map((selection) => selection.script);
|
.map((selection) => selection.script);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectIfNotExists(
|
function selectIfNotExists(
|
||||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||||
test: ReadonlyArray<SelectedScript>) {
|
test: ReadonlyArray<SelectedScript>,
|
||||||
|
) {
|
||||||
return selectableContainer
|
return selectableContainer
|
||||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||||
.map((selection) => selection.script);
|
.map((selection) => selection.script);
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
for (const line of lines) {
|
this.lines.push(...lines);
|
||||||
this.lines.push(line);
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public appendTrailingHyphensCommentLine(
|
public appendTrailingHyphensCommentLine(
|
||||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||||
|
): CodeBuilder {
|
||||||
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +44,8 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
|
|
||||||
public appendCommentLineWithHyphensAround(
|
public appendCommentLineWithHyphensAround(
|
||||||
sectionName: string,
|
sectionName: string,
|
||||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||||
|
): CodeBuilder {
|
||||||
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
||||||
if (sectionName.length >= totalRepeatHyphens) {
|
if (sectionName.length >= totalRepeatHyphens) {
|
||||||
return this.appendCommentLine(sectionName);
|
return this.appendCommentLine(sectionName);
|
||||||
@@ -63,5 +63,6 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract getCommentDelimiter(): string;
|
protected abstract getCommentDelimiter(): string;
|
||||||
|
|
||||||
protected abstract writeStandardOut(text: string): string;
|
protected abstract writeStandardOut(text: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ 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
|
||||||
|
extends ScriptingLanguageFactory<ICodeBuilder>
|
||||||
|
implements ICodeBuilderFactory {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export class BatchBuilder extends CodeBuilder {
|
|||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '::';
|
return '::';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo ${escapeForEcho(text)}`;
|
return `echo ${escapeForEcho(text)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export class ShellBuilder extends CodeBuilder {
|
|||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '#';
|
return '#';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo '${escapeForEcho(text)}'`;
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
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';
|
||||||
@@ -12,20 +12,21 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildCode(
|
public buildCode(
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
scriptingDefinition: IScriptingDefinition,
|
||||||
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||||
for (const selection of selectedScripts) {
|
const scriptPositions = selectedScripts.reduce((result, selection) => {
|
||||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
return appendSelection(selection, result, builder);
|
||||||
}
|
}, new Map<SelectedScript, ICodePosition>());
|
||||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||||
return { code, scriptPositions };
|
return { code, scriptPositions };
|
||||||
}
|
}
|
||||||
@@ -52,9 +53,11 @@ function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
|||||||
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 startPosition = builder.currentLine + 1;
|
||||||
|
appendCode(selection, builder);
|
||||||
const endPosition = builder.currentLine - 1;
|
const endPosition = builder.currentLine - 1;
|
||||||
builder.appendLine();
|
builder.appendLine();
|
||||||
const position = new CodePosition(startPosition, endPosition);
|
const position = new CodePosition(startPosition, endPosition);
|
||||||
@@ -63,8 +66,9 @@ function appendSelection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
||||||
return builder
|
return builder
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendFunction(name, scriptCode);
|
.appendFunction(name, scriptCode);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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>;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export class CodePosition implements ICodePosition {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly startLine: number,
|
public readonly startLine: number,
|
||||||
public readonly endLine: number) {
|
public readonly endLine: number,
|
||||||
|
) {
|
||||||
if (startLine < 0) {
|
if (startLine < 0) {
|
||||||
throw new Error('Code cannot start in a negative line');
|
throw new Error('Code cannot start in a negative line');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
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 (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
||||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasAnyMatches(): boolean {
|
public hasAnyMatches(): boolean {
|
||||||
return this.scriptMatches.length > 0
|
return this.scriptMatches.length > 0
|
||||||
|| this.categoryMatches.length > 0;
|
|| this.categoryMatches.length > 0;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserFilter extends IReadOnlyUserFilter {
|
||||||
setFilter(filter: string): void;
|
setFilter(filter: string): void;
|
||||||
removeFilter(): void;
|
removeFilter(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
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 readonly filterRemoved = new EventSource<void>();
|
||||||
|
|
||||||
public currentFilter: IFilterResult | undefined;
|
public currentFilter: IFilterResult | undefined;
|
||||||
|
|
||||||
constructor(private collection: ICategoryCollection) {
|
constructor(private collection: ICategoryCollection) {
|
||||||
@@ -20,9 +22,11 @@ export class UserFilter implements IUserFilter {
|
|||||||
}
|
}
|
||||||
const filterLowercase = filter.toLocaleLowerCase();
|
const filterLowercase = filter.toLocaleLowerCase();
|
||||||
const filteredScripts = this.collection.getAllScripts().filter(
|
const filteredScripts = this.collection.getAllScripts().filter(
|
||||||
(script) => isScriptAMatch(script, filterLowercase));
|
(script) => isScriptAMatch(script, filterLowercase),
|
||||||
|
);
|
||||||
const filteredCategories = this.collection.getAllCategories().filter(
|
const filteredCategories = this.collection.getAllCategories().filter(
|
||||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
(category) => category.name.toLowerCase().includes(filterLowercase),
|
||||||
|
);
|
||||||
const matches = new FilterResult(
|
const matches = new FilterResult(
|
||||||
filteredScripts,
|
filteredScripts,
|
||||||
filteredCategories,
|
filteredCategories,
|
||||||
|
|||||||
@@ -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 os: OperatingSystem;
|
||||||
|
readonly filter: IReadOnlyUserFilter;
|
||||||
|
readonly selection: IReadOnlyUserSelection;
|
||||||
|
readonly collection: ICategoryCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||||
readonly filter: IUserFilter;
|
readonly filter: IUserFilter;
|
||||||
readonly selection: IUserSelection;
|
readonly selection: IUserSelection;
|
||||||
readonly collection: ICategoryCollection;
|
|
||||||
readonly os: OperatingSystem;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
isSelected(scriptId: string): boolean;
|
||||||
areAllSelected(category: ICategory): boolean;
|
areAllSelected(category: ICategory): boolean;
|
||||||
isAnySelected(category: ICategory): boolean;
|
isAnySelected(category: ICategory): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserSelection extends IReadOnlyUserSelection {
|
||||||
removeAllInCategory(categoryId: number): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
removeSelectedScript(scriptId: string): void;
|
removeSelectedScript(scriptId: string): void;
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||||
isSelected(scriptId: string): boolean;
|
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
deselectAll(): void;
|
deselectAll(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
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>;
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly collection: ICategoryCollection,
|
private readonly collection: ICategoryCollection,
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
) {
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public areAllSelected(category: ICategory): boolean {
|
public areAllSelected(category: ICategory): boolean {
|
||||||
if (this.selectedScripts.length === 0) {
|
if (this.selectedScripts.length === 0) {
|
||||||
@@ -30,7 +30,9 @@ export class UserSelection implements IUserSelection {
|
|||||||
if (this.selectedScripts.length < scripts.length) {
|
if (this.selectedScripts.length < scripts.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
|
return scripts.every(
|
||||||
|
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAnySelected(category: ICategory): boolean {
|
public isAnySelected(category: ICategory): boolean {
|
||||||
@@ -53,19 +55,20 @@ export class UserSelection implements IUserSelection {
|
|||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||||
const category = this.collection.findCategory(categoryId);
|
const scriptsToAddOrUpdate = this.collection
|
||||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
.findCategory(categoryId)
|
||||||
.filter((script) =>
|
.getAllScriptsRecursively()
|
||||||
!this.scripts.exists(script.id)
|
.filter(
|
||||||
|
(script) => !this.scripts.exists(script.id)
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
|| this.scripts.getById(script.id).revert !== revert,
|
||||||
);
|
)
|
||||||
|
.map((script) => new SelectedScript(script, revert));
|
||||||
if (!scriptsToAddOrUpdate.length) {
|
if (!scriptsToAddOrUpdate.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const script of scriptsToAddOrUpdate) {
|
for (const script of scriptsToAddOrUpdate) {
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
this.scripts.addOrUpdateItem(script);
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
@@ -102,11 +105,12 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
for (const script of this.collection.getAllScripts()) {
|
const scriptsToSelect = this.collection
|
||||||
if (!this.scripts.exists(script.id)) {
|
.getAllScripts()
|
||||||
const selection = new SelectedScript(script, false);
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
this.scripts.addItem(selection);
|
.map((script) => new SelectedScript(script, false));
|
||||||
}
|
for (const script of scriptsToSelect) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
@@ -131,10 +135,11 @@ export class UserSelection implements IUserSelection {
|
|||||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||||
}
|
}
|
||||||
// Select from unselected scripts
|
// Select from unselected scripts
|
||||||
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
const unselectedScripts = scripts
|
||||||
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
|
.map((script) => new SelectedScript(script, false));
|
||||||
for (const toSelect of unselectedScripts) {
|
for (const toSelect of unselectedScripts) {
|
||||||
const selection = new SelectedScript(toSelect, false);
|
this.scripts.addItem(toSelect);
|
||||||
this.scripts.addItem(selection);
|
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 {
|
public detect(userAgent: string): OperatingSystem | undefined {
|
||||||
if (!userAgent) {
|
if (!userAgent) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -19,35 +20,37 @@ export class BrowserOsDetector implements IBrowserOsDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
||||||
|
os: OperatingSystem,
|
||||||
|
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
|
||||||
|
): IBrowserOsDetector {
|
||||||
const builder = new DetectorBuilder(os);
|
const builder = new DetectorBuilder(os);
|
||||||
applyRules(builder);
|
applyRules(builder);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
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>();
|
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||||
|
|
||||||
constructor(private readonly os: OperatingSystem) { }
|
constructor(private readonly os: OperatingSystem) { }
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
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;
|
||||||
@@ -12,21 +12,28 @@ interface IEnvironmentVariables {
|
|||||||
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 isDesktop: boolean;
|
||||||
|
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
variables: IEnvironmentVariables,
|
variables: IEnvironmentVariables,
|
||||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||||
|
) {
|
||||||
if (!variables) {
|
if (!variables) {
|
||||||
throw new Error('variables is null or empty');
|
throw new Error('variables is null or empty');
|
||||||
}
|
}
|
||||||
this.isDesktop = isDesktop(variables);
|
this.isDesktop = isDesktop(variables);
|
||||||
this.os = this.isDesktop ?
|
if (this.isDesktop) {
|
||||||
getDesktopOsType(getProcessPlatform(variables))
|
this.os = getDesktopOsType(getProcessPlatform(variables));
|
||||||
: browserOsDetector.detect(getUserAgent(variables));
|
} else {
|
||||||
|
const userAgent = getUserAgent(variables);
|
||||||
|
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,14 +53,16 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
|
|||||||
|
|
||||||
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) {
|
||||||
|
case 'darwin':
|
||||||
return OperatingSystem.macOS;
|
return OperatingSystem.macOS;
|
||||||
} else if (processPlatform === 'win32') {
|
case 'win32':
|
||||||
return OperatingSystem.Windows;
|
return OperatingSystem.Windows;
|
||||||
} else if (processPlatform === 'linux') {
|
case 'linux':
|
||||||
return OperatingSystem.Linux;
|
return OperatingSystem.Linux;
|
||||||
}
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
|
|||||||
@@ -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,17 +1,18 @@
|
|||||||
|
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,
|
||||||
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData);
|
validateCollectionsData(collectionsData);
|
||||||
const information = parseProjectInformation(processEnv);
|
const information = parseProjectInformation(processEnv);
|
||||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||||
@@ -22,11 +23,13 @@ export function parseApplication(
|
|||||||
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) {
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
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),
|
||||||
|
): ICategoryCollection {
|
||||||
validate(content);
|
validate(content);
|
||||||
const scripting = new ScriptingDefinitionParser()
|
const scripting = new ScriptingDefinitionParser()
|
||||||
.parse(content.scripting, info);
|
.parse(content.scripting, info);
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
const categories = new Array<Category>();
|
const categories = content.actions.map((action) => parseCategory(action, context));
|
||||||
for (const action of content.actions) {
|
|
||||||
const category = parseCategory(action, context);
|
|
||||||
categories.push(category);
|
|
||||||
}
|
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
const collection = new CategoryCollection(
|
const collection = new CategoryCollection(
|
||||||
os,
|
os,
|
||||||
categories,
|
categories,
|
||||||
scripting);
|
scripting,
|
||||||
|
);
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
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 {
|
||||||
|
|
||||||
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
|
||||||
if (!context) { throw new Error('undefined context'); }
|
if (!context) { throw new Error('undefined context'); }
|
||||||
ensureValid(category);
|
ensureValid(category);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
@@ -23,11 +23,11 @@ export function parseCategory(category: CategoryData, context: ICategoryCollecti
|
|||||||
parseCategoryChild(data, children, category, context);
|
parseCategoryChild(data, children, category, context);
|
||||||
}
|
}
|
||||||
return new Category(
|
return new Category(
|
||||||
/*id*/ categoryIdCounter++,
|
/* id: */ categoryIdCounter++,
|
||||||
/*name*/ category.category,
|
/* name: */ category.category,
|
||||||
/*docs*/ parseDocUrls(category),
|
/* docs: */ parseDocUrls(category),
|
||||||
/*categories*/ children.subCategories,
|
/* categories: */ children.subCategories,
|
||||||
/*scripts*/ children.subScripts,
|
/* scripts: */ children.subScripts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +43,17 @@ function ensureValid(category: CategoryData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)) {
|
if (isCategory(data)) {
|
||||||
const subCategory = parseCategory(data as CategoryData, context);
|
const subCategory = parseCategory(data as CategoryData, context);
|
||||||
children.subCategories.push(subCategory);
|
children.subCategories.push(subCategory);
|
||||||
@@ -61,11 +67,20 @@ function parseCategoryChild(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
|
|||||||
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 [];
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,10 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
|
|||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
function addDocs(
|
||||||
|
docs: DocumentationUrlsData,
|
||||||
|
urls: DocumentationUrlContainer,
|
||||||
|
): DocumentationUrlContainer {
|
||||||
if (docs instanceof Array) {
|
if (docs instanceof Array) {
|
||||||
urls.addUrls(docs);
|
urls.addUrls(docs);
|
||||||
} else if (typeof docs === 'string') {
|
} else if (typeof docs === 'string') {
|
||||||
@@ -32,7 +35,7 @@ class DocumentationUrlContainer {
|
|||||||
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');
|
||||||
@@ -53,8 +56,8 @@ function validateUrl(docUrl: string): void {
|
|||||||
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,7 +2,8 @@ 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,
|
||||||
|
): IProjectInformation {
|
||||||
return new ProjectInformation(
|
return new ProjectInformation(
|
||||||
environment.VUE_APP_NAME,
|
environment.VUE_APP_NAME,
|
||||||
environment.VUE_APP_VERSION,
|
environment.VUE_APP_VERSION,
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -9,12 +9,14 @@ 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;
|
public readonly syntax: ILanguageSyntax;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
scripting: IScriptingDefinition,
|
scripting: IScriptingDefinition,
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
|
) {
|
||||||
if (!scripting) { throw new Error('undefined scripting'); }
|
if (!scripting) { throw new Error('undefined scripting'); }
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
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
|
||||||
|
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
|
||||||
|
) {
|
||||||
if (!position) {
|
if (!position) {
|
||||||
throw new Error('undefined position');
|
throw new Error('undefined position');
|
||||||
}
|
}
|
||||||
@@ -20,14 +21,15 @@ export class Expression implements IExpression {
|
|||||||
throw new Error('undefined evaluator');
|
throw new Error('undefined evaluator');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('undefined context');
|
throw new Error('undefined context');
|
||||||
}
|
}
|
||||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
const args = filterUnusedArguments(this.parameters, context.args);
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
return this.evaluator(context);
|
return this.evaluator(filteredContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,20 +45,19 @@ function validateThatAllRequiredParametersAreSatisfied(
|
|||||||
.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,
|
||||||
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||||
for (const parameter of parameters.all) {
|
parameters.all
|
||||||
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
|
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
|
||||||
continue; // Optional parameter is not necessarily provided
|
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
|
||||||
}
|
.forEach((argument) => specificCallArgs.addArgument(argument));
|
||||||
const arg = allFunctionArgs.getArgument(parameter.name);
|
|
||||||
specificCallArgs.addArgument(arg);
|
|
||||||
}
|
|
||||||
return specificCallArgs;
|
return specificCallArgs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export interface IExpressionEvaluationContext {
|
|||||||
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) {
|
if (!args) {
|
||||||
throw new Error('undefined args, send empty collection instead');
|
throw new Error('undefined args, send empty collection instead');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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) {
|
if (start === end) {
|
||||||
throw new Error(`no length (start = end = ${start})`);
|
throw new Error(`no length (start = end = ${start})`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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 {
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
|
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(
|
public compileExpressions(
|
||||||
code: string,
|
code: string | undefined,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string {
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): string {
|
||||||
if (!args) {
|
if (!args) {
|
||||||
throw new Error('undefined args, send empty collection instead');
|
throw new Error('undefined args, send empty collection instead');
|
||||||
}
|
}
|
||||||
|
if (!code) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
const expressions = this.extractor.findExpressions(code);
|
const expressions = this.extractor.findExpressions(code);
|
||||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||||
const context = new ExpressionEvaluationContext(args);
|
const context = new ExpressionEvaluationContext(args);
|
||||||
@@ -26,7 +31,8 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
function compileExpressions(
|
function compileExpressions(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
code: string,
|
code: string,
|
||||||
context: IExpressionEvaluationContext) {
|
context: IExpressionEvaluationContext,
|
||||||
|
) {
|
||||||
let compiledCode = '';
|
let compiledCode = '';
|
||||||
const sortedExpressions = expressions
|
const sortedExpressions = expressions
|
||||||
.slice() // copy the array to not mutate the parameter
|
.slice() // copy the array to not mutate the parameter
|
||||||
@@ -48,20 +54,21 @@ function compileExpressions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractRequiredParameterNames(
|
function extractRequiredParameterNames(
|
||||||
expressions: readonly IExpression[]): string[] {
|
expressions: readonly IExpression[],
|
||||||
const usedParameterNames = expressions
|
): string[] {
|
||||||
|
return expressions
|
||||||
.map((e) => e.parameters.all
|
.map((e) => e.parameters.all
|
||||||
.filter((p) => !p.isOptional)
|
.filter((p) => !p.isOptional)
|
||||||
.map((p) => p.name))
|
.map((p) => p.name))
|
||||||
.filter((p) => p)
|
.filter(Boolean) // Remove empty or undefined
|
||||||
.flat();
|
.flat()
|
||||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||||
return uniqueParameterNames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureParamsUsedInCodeHasArgsProvided(
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): void {
|
||||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
if (!usedParameterNames?.length) {
|
if (!usedParameterNames?.length) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
|
|||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
code: string,
|
code: string | undefined,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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(),
|
||||||
@@ -14,14 +14,10 @@ export class CompositeExpressionParser implements IExpressionParser {
|
|||||||
throw new Error('undefined leaf');
|
throw new Error('undefined leaf');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
const expressions = new Array<IExpression>();
|
return this.leafs.flatMap(
|
||||||
for (const parser of this.leafs) {
|
(parser) => parser.findExpressions(code) || [],
|
||||||
const newExpressions = parser.findExpressions(code);
|
);
|
||||||
if (newExpressions && newExpressions.length) {
|
|
||||||
expressions.push(...newExpressions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expressions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export class ExpressionRegexBuilder {
|
|||||||
return this
|
return this
|
||||||
.addRawRegex('\\s*');
|
.addRawRegex('\\s*');
|
||||||
}
|
}
|
||||||
|
|
||||||
private addRawRegex(regex: string) {
|
private addRawRegex(regex: string) {
|
||||||
this.parts.push(regex);
|
this.parts.push(regex);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -15,34 +15,45 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||||
|
|
||||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
const matches = Array.from(code.matchAll(this.regex));
|
if (!code) {
|
||||||
for (const match of matches) {
|
throw new Error('undefined code');
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
let position: ExpressionPosition;
|
|
||||||
try {
|
|
||||||
position = new ExpressionPosition(startPos, endPos);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
|
||||||
}
|
}
|
||||||
|
const matches = code.matchAll(this.regex);
|
||||||
|
for (const match of matches) {
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.buildExpression(match);
|
||||||
const parameters = getParameters(primitiveExpression);
|
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
||||||
|
const parameters = createParameters(primitiveExpression);
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
yield expression;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ 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 {
|
public apply(raw: string): string {
|
||||||
return raw?.replaceAll('"', '"^""');
|
return raw?.replaceAll('"', '"^""');
|
||||||
|
/* eslint-disable max-len */
|
||||||
/*
|
/*
|
||||||
"^"" is the most robust and stable choice.
|
"^"" is the most robust and stable choice.
|
||||||
Other options:
|
Other options:
|
||||||
@@ -11,17 +13,18 @@ export class EscapeDoubleQuotes implements IPipe {
|
|||||||
Breaks, because it is fundamentally unsupported
|
Breaks, because it is fundamentally unsupported
|
||||||
""""
|
""""
|
||||||
Does not work with consecutive double quotes.
|
Does not work with consecutive double quotes.
|
||||||
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
|
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
|
||||||
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
|
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
|
||||||
\"
|
\"
|
||||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
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 "&"
|
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"^""'"
|
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
|
||||||
\""
|
\""
|
||||||
Normalizes interior whitespace
|
Normalizes interior whitespace
|
||||||
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
|
E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
|
||||||
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
|
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||||
A good explanation: https://stackoverflow.com/a/31413730
|
A good explanation: https://stackoverflow.com/a/31413730
|
||||||
*/
|
*/
|
||||||
|
/* eslint-enable max-len */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ 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 {
|
public apply(code: string): string {
|
||||||
if (!code || !hasLines(code)) {
|
if (!code || !hasLines(code)) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
code = replaceComments(code);
|
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||||
code = mergeLinesWithBacktick(code);
|
inlineComments,
|
||||||
code = mergeHereStrings(code);
|
mergeLinesWithBacktick,
|
||||||
const lines = getLines(code)
|
mergeHereStrings,
|
||||||
.map((line) => line.trim())
|
mergeNewLines,
|
||||||
.filter((line) => line.length > 0);
|
]).reduce((a, b) => (data) => b(a(data)));
|
||||||
return lines
|
const newCode = processor(code);
|
||||||
.join('; ');
|
return newCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,18 +26,71 @@ function hasLines(text: string) {
|
|||||||
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/) || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -59,15 +113,18 @@ interface IInlinedHereString {
|
|||||||
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 {
|
||||||
|
/*
|
||||||
|
We handle @' and @" differently.
|
||||||
|
Single quotes are interpreted literally and doubles are expandable.
|
||||||
|
*/
|
||||||
const expandableNewLine = '`r`n';
|
const expandableNewLine = '`r`n';
|
||||||
switch (quotes) {
|
switch (quotes) {
|
||||||
case '\'':
|
case '\'':
|
||||||
return {
|
return {
|
||||||
quotesAround: '\'',
|
quotesAround: '\'',
|
||||||
escapedQuotes: '\'\'',
|
escapedQuotes: '\'\'',
|
||||||
separator: `\'+"${expandableNewLine}"+\'`,
|
separator: `'+"${expandableNewLine}"+'`,
|
||||||
};
|
};
|
||||||
case '"':
|
case '"':
|
||||||
return {
|
return {
|
||||||
@@ -100,3 +157,10 @@ function mergeLinesWithBacktick(code: string) {
|
|||||||
*/
|
*/
|
||||||
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('; ');
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface IPipeFactory {
|
|||||||
|
|
||||||
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) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
if (pipes.some((pipe) => !pipe)) {
|
||||||
throw new Error('undefined pipe in list');
|
throw new Error('undefined pipe in list');
|
||||||
@@ -21,6 +22,7 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(pipeName: string): IPipe {
|
public get(pipeName: string): IPipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
if (!this.pipes.has(pipeName)) {
|
if (!this.pipes.has(pipeName)) {
|
||||||
@@ -28,6 +30,7 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
}
|
}
|
||||||
return this.pipes.get(pipeName);
|
return this.pipes.get(pipeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: IPipe): void {
|
private registerPipe(pipe: IPipe): void {
|
||||||
validatePipeName(pipe.name);
|
validatePipeName(pipe.name);
|
||||||
if (this.pipes.has(pipe.name)) {
|
if (this.pipes.has(pipe.name)) {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ 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 {
|
public compile(value: string, pipeline: string): string {
|
||||||
ensureValidArguments(value, pipeline);
|
ensureValidArguments(value, pipeline);
|
||||||
const pipeNames = extractPipeNames(pipeline);
|
const pipeNames = extractPipeNames(pipeline);
|
||||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||||
for (const pipe of pipes) {
|
return pipes.reduce((previousValue, pipe) => {
|
||||||
value = pipe.apply(value);
|
return pipe.apply(previousValue);
|
||||||
}
|
}, value);
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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 {
|
||||||
@@ -15,9 +15,9 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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 {
|
||||||
@@ -23,10 +23,10 @@ export class WithParser extends RegexParser {
|
|||||||
const parameterName = match[1];
|
const parameterName = match[1];
|
||||||
const scopeText = match[2];
|
const scopeText = match[2];
|
||||||
return {
|
return {
|
||||||
parameters: [ new FunctionParameter(parameterName, true) ],
|
parameters: [new FunctionParameter(parameterName, true)],
|
||||||
evaluator: (context) => {
|
evaluator: (context) => {
|
||||||
const argumentValue = context.args.hasArgument(parameterName) ?
|
const argumentValue = context.args.hasArgument(parameterName)
|
||||||
context.args.getArgument(parameterName).argumentValue
|
? context.args.getArgument(parameterName).argumentValue
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
return '';
|
return '';
|
||||||
@@ -43,7 +43,7 @@ export class WithParser extends RegexParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||||
// {{ . | pipeName }}
|
// {{ . | pipeName }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('.')
|
.expectCharacters('.')
|
||||||
.matchPipeline() // First match: pipeline
|
.matchPipeline() // First match: pipeline
|
||||||
@@ -51,8 +51,9 @@ const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
|||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
|
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
|
// but instead letting the pipeline compiler to fail on those.
|
||||||
|
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||||
return replacer(match1);
|
return replacer(match1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
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);
|
ensureValidParameterName(parameterName);
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
throw new Error(`undefined argument value for "${parameterName}"`);
|
throw new Error(`undefined argument value for "${parameterName}"`);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollecti
|
|||||||
|
|
||||||
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 {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
if (!argument) {
|
if (!argument) {
|
||||||
throw new Error('undefined argument');
|
throw new Error('undefined argument');
|
||||||
@@ -12,15 +13,18 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
}
|
}
|
||||||
this.arguments.set(argument.parameterName, argument);
|
this.arguments.set(argument.parameterName, argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllParameterNames(): string[] {
|
public getAllParameterNames(): string[] {
|
||||||
return Array.from(this.arguments.keys());
|
return Array.from(this.arguments.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasArgument(parameterName: string): boolean {
|
public hasArgument(parameterName: string): boolean {
|
||||||
if (!parameterName) {
|
if (!parameterName) {
|
||||||
throw new Error('undefined parameter name');
|
throw new Error('undefined parameter name');
|
||||||
}
|
}
|
||||||
return this.arguments.has(parameterName);
|
return this.arguments.has(parameterName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getArgument(parameterName: string): IFunctionCallArgument {
|
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||||
if (!parameterName) {
|
if (!parameterName) {
|
||||||
throw new Error('undefined parameter name');
|
throw new Error('undefined parameter name');
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
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,
|
||||||
|
): ICompiledCode {
|
||||||
if (!functions) { throw new Error('undefined functions'); }
|
if (!functions) { throw new Error('undefined functions'); }
|
||||||
if (!calls) { throw new Error('undefined calls'); }
|
if (!calls) { throw new Error('undefined calls'); }
|
||||||
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
||||||
@@ -45,24 +47,25 @@ interface ICompiledFunctionCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
compiledFunctions.push(...compiledCode);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
code: merge(compiledFunctions.map((f) => f.code)),
|
code: merge(compiledFunctions.map((f) => f.code)),
|
||||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] {
|
function compileSingleCall(
|
||||||
|
call: IFunctionCall,
|
||||||
|
context: ICompilationContext,
|
||||||
|
): ICompiledFunctionCall[] {
|
||||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||||
if (func.body.code) { // Function with inline code
|
if (func.body.code) { // Function with inline code
|
||||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||||
return [ compiledCode ];
|
return [compiledCode];
|
||||||
} else { // Function with inner calls
|
}
|
||||||
|
// Function with inner calls
|
||||||
return func.body.calls
|
return func.body.calls
|
||||||
.map((innerCall) => {
|
.map((innerCall) => {
|
||||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||||
@@ -70,13 +73,13 @@ function compileSingleCall(call: IFunctionCall, context: ICompilationContext): I
|
|||||||
return compileSingleCall(compiledCall, context);
|
return compileSingleCall(compiledCall, context);
|
||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileCode(
|
function compileCode(
|
||||||
code: IFunctionCode,
|
code: IFunctionCode,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
compiler: IExpressionsCompiler): ICompiledFunctionCall {
|
compiler: IExpressionsCompiler,
|
||||||
|
): ICompiledFunctionCall {
|
||||||
return {
|
return {
|
||||||
code: compiler.compileExpressions(code.do, args),
|
code: compiler.compileExpressions(code.do, args),
|
||||||
revertCode: compiler.compileExpressions(code.revert, args),
|
revertCode: compiler.compileExpressions(code.revert, args),
|
||||||
@@ -87,15 +90,18 @@ 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 { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||||
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
|
return new FunctionCallArgument(parameterName, compiledValue);
|
||||||
compiledArgs.addArgument(newArgument);
|
})
|
||||||
}
|
.reduce((compiledArgs, arg) => {
|
||||||
|
compiledArgs.addArgument(arg);
|
||||||
return compiledArgs;
|
return compiledArgs;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
function merge(codeParts: readonly string[]): string {
|
function merge(codeParts: readonly string[]): string {
|
||||||
@@ -106,7 +112,8 @@ function merge(codeParts: readonly string[]): string {
|
|||||||
|
|
||||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||||
func: ISharedFunction,
|
func: ISharedFunction,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): void {
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): void {
|
||||||
const callArgumentNames = args.getAllParameterNames();
|
const callArgumentNames = args.getAllParameterNames();
|
||||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||||
@@ -115,7 +122,8 @@ function ensureThatCallArgumentsExistInParameterDefinition(
|
|||||||
|
|
||||||
function findUnexpectedParameters(
|
function findUnexpectedParameters(
|
||||||
callArgumentNames: string[],
|
callArgumentNames: string[],
|
||||||
functionParameterNames: string[]): string[] {
|
functionParameterNames: string[],
|
||||||
|
): string[] {
|
||||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -126,14 +134,16 @@ function findUnexpectedParameters(
|
|||||||
function throwIfNotEmpty(
|
function throwIfNotEmpty(
|
||||||
functionName: string,
|
functionName: string,
|
||||||
unexpectedParameters: string[],
|
unexpectedParameters: string[],
|
||||||
expectedParameters: string[]) {
|
expectedParameters: string[],
|
||||||
|
) {
|
||||||
if (!unexpectedParameters.length) {
|
if (!unexpectedParameters.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: ` +
|
// eslint-disable-next-line prefer-template
|
||||||
`"${unexpectedParameters.join('", "')}"` +
|
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||||
'. Expected parameter(s): ' +
|
+ `"${unexpectedParameters.join('", "')}"`
|
||||||
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
+ '. Expected parameter(s): '
|
||||||
|
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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(
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ 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) {
|
if (!functionName) {
|
||||||
throw new Error('empty function name in function call');
|
throw new Error('empty function name in function call');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
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);
|
||||||
@@ -19,17 +19,24 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
|||||||
if (calls instanceof Array) {
|
if (calls instanceof Array) {
|
||||||
return calls as FunctionCallData[];
|
return calls as FunctionCallData[];
|
||||||
}
|
}
|
||||||
return [ calls as FunctionCallData ];
|
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,5 +1,5 @@
|
|||||||
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;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { IFunctionParameter } from './IFunctionParameter';
|
|
||||||
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
|
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
|
||||||
|
import { IFunctionParameter } from './IFunctionParameter';
|
||||||
|
|
||||||
export class FunctionParameter implements IFunctionParameter {
|
export class FunctionParameter implements IFunctionParameter {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly isOptional: boolean) {
|
public readonly isOptional: boolean,
|
||||||
|
) {
|
||||||
ensureValidParameterName(name);
|
ensureValidParameterName(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
|||||||
public get all(): readonly IFunctionParameter[] {
|
public get all(): readonly IFunctionParameter[] {
|
||||||
return this.parameters;
|
return this.parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addParameter(parameter: IFunctionParameter) {
|
public addParameter(parameter: IFunctionParameter) {
|
||||||
this.ensureValidParameter(parameter);
|
this.ensureValidParameter(parameter);
|
||||||
this.parameters.push(parameter);
|
this.parameters.push(parameter);
|
||||||
@@ -15,6 +16,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
|||||||
private includesName(name: string) {
|
private includesName(name: string) {
|
||||||
return this.parameters.find((existingParameter) => existingParameter.name === name);
|
return this.parameters.find((existingParameter) => existingParameter.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||||
if (!parameter) {
|
if (!parameter) {
|
||||||
throw new Error('undefined parameter');
|
throw new Error('undefined parameter');
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
import { IFunctionCall } from './Call/IFunctionCall';
|
||||||
import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction';
|
import {
|
||||||
|
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||||
|
} from './ISharedFunction';
|
||||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
export function createCallerFunction(
|
export function createCallerFunction(
|
||||||
name: string,
|
name: string,
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
callSequence: readonly IFunctionCall[]): ISharedFunction {
|
callSequence: readonly IFunctionCall[],
|
||||||
|
): ISharedFunction {
|
||||||
if (!callSequence) {
|
if (!callSequence) {
|
||||||
throw new Error(`undefined call sequence in function "${name}"`);
|
throw new Error(`undefined call sequence in function "${name}"`);
|
||||||
}
|
}
|
||||||
@@ -19,7 +22,8 @@ export function createFunctionWithInlineCode(
|
|||||||
name: string,
|
name: string,
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
code: string,
|
code: string,
|
||||||
revertCode?: string): ISharedFunction {
|
revertCode?: string,
|
||||||
|
): ISharedFunction {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error(`undefined code in function "${name}"`);
|
throw new Error(`undefined code in function "${name}"`);
|
||||||
}
|
}
|
||||||
@@ -32,6 +36,7 @@ export function createFunctionWithInlineCode(
|
|||||||
|
|
||||||
class SharedFunction implements ISharedFunction {
|
class SharedFunction implements ISharedFunction {
|
||||||
public readonly body: ISharedFunctionBody;
|
public readonly body: ISharedFunctionBody;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
||||||
@@ -39,7 +44,7 @@ class SharedFunction implements ISharedFunction {
|
|||||||
bodyType: FunctionBodyType,
|
bodyType: FunctionBodyType,
|
||||||
) {
|
) {
|
||||||
if (!name) { throw new Error('undefined function name'); }
|
if (!name) { throw new Error('undefined function name'); }
|
||||||
if (!parameters) { throw new Error(`undefined parameters`); }
|
if (!parameters) { throw new Error('undefined parameters'); }
|
||||||
this.body = {
|
this.body = {
|
||||||
type: bodyType,
|
type: bodyType,
|
||||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||||
|
|||||||
@@ -11,47 +11,51 @@ import { parseFunctionCalls } from './Call/FunctionCallParser';
|
|||||||
|
|
||||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||||
|
|
||||||
public parseFunctions(
|
public parseFunctions(
|
||||||
functions: readonly FunctionData[]): ISharedFunctionCollection {
|
functions: readonly FunctionData[],
|
||||||
|
): ISharedFunctionCollection {
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
if (!functions || !functions.length) {
|
if (!functions || !functions.length) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
for (const func of functions) {
|
return functions
|
||||||
const sharedFunction = parseFunction(func);
|
.map((func) => parseFunction(func))
|
||||||
collection.addFunction(sharedFunction);
|
.reduce((acc, func) => {
|
||||||
}
|
acc.addFunction(func);
|
||||||
return collection;
|
return acc;
|
||||||
|
}, collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunction(data: FunctionData): ISharedFunction {
|
function parseFunction(data: FunctionData): ISharedFunction {
|
||||||
const name = data.name;
|
const { name } = data;
|
||||||
const parameters = parseParameters(data);
|
const parameters = parseParameters(data);
|
||||||
if (hasCode(data)) {
|
if (hasCode(data)) {
|
||||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
} else { // has call
|
}
|
||||||
|
// Has call
|
||||||
const calls = parseFunctionCalls(data.call);
|
const calls = parseFunctionCalls(data.call);
|
||||||
return createCallerFunction(name, parameters, calls);
|
return createCallerFunction(name, parameters, calls);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
const parameters = new FunctionParameterCollection();
|
return (data.parameters || [])
|
||||||
if (!data.parameters) {
|
.map((parameter) => {
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
for (const parameterData of data.parameters) {
|
|
||||||
const isOptional = parameterData.optional || false;
|
|
||||||
try {
|
try {
|
||||||
const parameter = new FunctionParameter(parameterData.name, isOptional);
|
return new FunctionParameter(
|
||||||
parameters.addParameter(parameter);
|
parameter.name,
|
||||||
|
parameter.optional || false,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`"${data.name}": ${err.message}`);
|
throw new Error(`"${data.name}": ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.reduce((parameters, parameter) => {
|
||||||
|
parameters.addParameter(parameter);
|
||||||
return parameters;
|
return parameters;
|
||||||
|
}, new FunctionParameterCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: FunctionData): boolean {
|
function hasCode(data: FunctionData): boolean {
|
||||||
@@ -96,7 +100,7 @@ function ensureExpectedParametersType(functions: readonly FunctionData[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isArrayOfObjects(value: any): boolean {
|
function isArrayOfObjects(value: unknown): boolean {
|
||||||
return Array.isArray(value)
|
return Array.isArray(value)
|
||||||
&& value.every((item) => typeof item === 'object');
|
&& value.every((item) => typeof item === 'object');
|
||||||
}
|
}
|
||||||
@@ -115,15 +119,14 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
|||||||
|
|
||||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||||
if (functions.some((func) => !func)) {
|
if (functions.some((func) => !func)) {
|
||||||
throw new Error(`some functions are undefined`);
|
throw new Error('some functions are undefined');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||||
const duplicateCodes = getDuplicates(functions
|
const duplicateCodes = getDuplicates(functions
|
||||||
.map((func) => func.code)
|
.map((func) => func.code)
|
||||||
.filter((code) => code),
|
.filter((code) => code));
|
||||||
);
|
|
||||||
if (duplicateCodes.length > 0) {
|
if (duplicateCodes.length > 0) {
|
||||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
|
||||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
|
||||||
export interface IScriptCompiler {
|
export interface IScriptCompiler {
|
||||||
canCompile(script: ScriptData): boolean;
|
canCompile(script: ScriptData): boolean;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { IScriptCompiler } from './IScriptCompiler';
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||||
@@ -12,15 +11,17 @@ import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
|||||||
|
|
||||||
export class ScriptCompiler implements IScriptCompiler {
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
private readonly functions: ISharedFunctionCollection;
|
private readonly functions: ISharedFunctionCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[] | undefined,
|
||||||
private readonly syntax: ILanguageSyntax,
|
private readonly syntax: ILanguageSyntax,
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
|
||||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||||
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('undefined syntax'); }
|
if (!syntax) { throw new Error('undefined syntax'); }
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
if (!script) { throw new Error('undefined script'); }
|
if (!script) { throw new Error('undefined script'); }
|
||||||
if (!script.call) {
|
if (!script.call) {
|
||||||
@@ -28,6 +29,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compile(script: ScriptData): IScriptCode {
|
public compile(script: ScriptData): IScriptCode {
|
||||||
if (!script) { throw new Error('undefined script'); }
|
if (!script) { throw new Error('undefined script'); }
|
||||||
try {
|
try {
|
||||||
@@ -36,7 +38,8 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
return new ScriptCode(
|
return new ScriptCode(
|
||||||
compiledCode.code,
|
compiledCode.code,
|
||||||
compiledCode.revertCode,
|
compiledCode.revertCode,
|
||||||
this.syntax);
|
this.syntax,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw Error(`Script "${script.name}" ${error.message}`);
|
throw Error(`Script "${script.name}" ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import { Script } from '@/domain/Script';
|
|
||||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||||
import { parseDocUrls } from '../DocumentationParser';
|
import { Script } from '@/domain/Script';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { parseDocUrls } from '../DocumentationParser';
|
||||||
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
data: ScriptData, context: ICategoryCollectionParseContext,
|
data: ScriptData,
|
||||||
levelParser = createEnumParser(RecommendationLevel)): Script {
|
context: ICategoryCollectionParseContext,
|
||||||
|
levelParser = createEnumParser(RecommendationLevel),
|
||||||
|
): Script {
|
||||||
validateScript(data);
|
validateScript(data);
|
||||||
if (!context) { throw new Error('undefined context'); }
|
if (!context) { throw new Error('undefined context'); }
|
||||||
const script = new Script(
|
const script = new Script(
|
||||||
/* name */ data.name,
|
/* name: */ data.name,
|
||||||
/* code */ parseCode(data, context),
|
/* code: */ parseCode(data, context),
|
||||||
/* docs */ parseDocUrls(data),
|
/* docs: */ parseDocUrls(data),
|
||||||
/* level */ parseLevel(data.recommend, levelParser));
|
/* level: */ parseLevel(data.recommend, levelParser),
|
||||||
|
);
|
||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined {
|
function parseLevel(
|
||||||
|
level: string,
|
||||||
|
parser: IEnumParser<RecommendationLevel>,
|
||||||
|
): RecommendationLevel | undefined {
|
||||||
if (!level) {
|
if (!level) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||||
const BatchFileCommonCodeParts = [ '(', ')', 'else', '||' ];
|
const PowerShellCommonCodeParts = ['{', '}'];
|
||||||
const PowerShellCommonCodeParts = [ '{', '}' ];
|
|
||||||
|
|
||||||
export class BatchFileSyntax implements ILanguageSyntax {
|
export class BatchFileSyntax implements ILanguageSyntax {
|
||||||
public readonly commentDelimiters = [ 'REM', '::' ];
|
public readonly commentDelimiters = ['REM', '::'];
|
||||||
public readonly commonCodeParts = [ ...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts ];
|
|
||||||
|
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
|
||||||
export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
|
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
public readonly commentDelimiters = [ '#' ];
|
public readonly commentDelimiters = ['#'];
|
||||||
public readonly commonCodeParts = [ '(', ')', 'else', 'fi' ];
|
|
||||||
|
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { BatchFileSyntax } from './BatchFileSyntax';
|
|||||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
|
|
||||||
export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
|
export class SyntaxFactory
|
||||||
|
extends ScriptingLanguageFactory<ILanguageSyntax>
|
||||||
|
implements ISyntaxFactory {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compile
|
|||||||
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
|
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
|
||||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ICodeSubstituter } from './ICodeSubstituter';
|
|
||||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
|
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||||
|
|
||||||
export class CodeSubstituter implements ICodeSubstituter {
|
export class CodeSubstituter implements ICodeSubstituter {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -14,12 +14,13 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public substitute(code: string, info: IProjectInformation): string {
|
public substitute(code: string, info: IProjectInformation): string {
|
||||||
if (!code) { throw new Error('undefined code'); }
|
if (!code) { throw new Error('undefined code'); }
|
||||||
if (!info) { throw new Error('undefined info'); }
|
if (!info) { throw new Error('undefined info'); }
|
||||||
const args = new FunctionCallArgumentCollection();
|
const args = new FunctionCallArgumentCollection();
|
||||||
const substitute = (name: string, value: string) =>
|
const substitute = (name: string, value: string) => args
|
||||||
args.addArgument(new FunctionCallArgument(name, value));
|
.addArgument(new FunctionCallArgument(name, value));
|
||||||
substitute('homepage', info.homepage);
|
substitute('homepage', info.homepage);
|
||||||
substitute('version', info.version);
|
substitute('version', info.version);
|
||||||
substitute('date', this.date.toUTCString());
|
substitute('date', this.date.toUTCString());
|
||||||
@@ -29,7 +30,7 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createSubstituteCompiler(): IExpressionsCompiler {
|
function createSubstituteCompiler(): IExpressionsCompiler {
|
||||||
const parsers = [ new ParameterSubstitutionParser() ];
|
const parsers = [new ParameterSubstitutionParser()];
|
||||||
const parser = new CompositeExpressionParser(parsers);
|
const parser = new CompositeExpressionParser(parsers);
|
||||||
const expressionCompiler = new ExpressionsCompiler(parser);
|
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||||
return expressionCompiler;
|
return expressionCompiler;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
@@ -13,9 +13,11 @@ export class ScriptingDefinitionParser {
|
|||||||
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public parse(
|
public parse(
|
||||||
definition: ScriptingDefinitionData,
|
definition: ScriptingDefinitionData,
|
||||||
info: IProjectInformation): IScriptingDefinition {
|
info: IProjectInformation,
|
||||||
|
): IScriptingDefinition {
|
||||||
if (!info) { throw new Error('undefined info'); }
|
if (!info) { throw new Error('undefined info'); }
|
||||||
if (!definition) { throw new Error('undefined definition'); }
|
if (!definition) { throw new Error('undefined definition'); }
|
||||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||||
@@ -28,4 +30,3 @@ export class ScriptingDefinitionParser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ actions:
|
|||||||
name: Clear shared-cache strings data
|
name: Clear shared-cache strings data
|
||||||
docs:
|
docs:
|
||||||
- https://eclecticlight.co/2017/09/23/sierras-unified-log-evolves-more-persistent-and-a-valuable-log-log/
|
- https://eclecticlight.co/2017/09/23/sierras-unified-log-evolves-more-persistent-and-a-valuable-log-log/
|
||||||
- https://github.com/libyal/dtformats/blob/main/documentation/Apple%20Unified%20Logging%20and%20Activity%20Tracing%20formats.asciidoc
|
- https://github.com/privacysexy-forks/dtformats/blob/main/documentation/Apple%20Unified%20Logging%20and%20Activity%20Tracing%20formats.asciidoc
|
||||||
code: |-
|
code: |-
|
||||||
sudo rm -rfv /private/var/db/uuidtext/
|
sudo rm -rfv /private/var/db/uuidtext/
|
||||||
sudo rm -rfv /var/db/uuidtext/
|
sudo rm -rfv /var/db/uuidtext/
|
||||||
@@ -458,7 +458,7 @@ actions:
|
|||||||
-
|
-
|
||||||
name: Disable Firefox telemetry
|
name: Disable Firefox telemetry
|
||||||
recommend: standard
|
recommend: standard
|
||||||
docs: https://github.com/mozilla/policy-templates/blob/master/README.md
|
docs: https://github.com/privacysexy-forks/policy-templates/blob/master/README.md
|
||||||
code: |-
|
code: |-
|
||||||
# Enable Firefox policies so the telemetry can be configured.
|
# Enable Firefox policies so the telemetry can be configured.
|
||||||
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
|
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
|
||||||
@@ -503,7 +503,7 @@ actions:
|
|||||||
-
|
-
|
||||||
name: Disable PowerShell Core telemetry
|
name: Disable PowerShell Core telemetry
|
||||||
recommend: standard
|
recommend: standard
|
||||||
docs: https://github.com/PowerShell/PowerShell/blob/v7.1.0/README.md#telemetry
|
docs: https://github.com/privacysexy-forks/PowerShell/blob/v7.1.5/README.md#telemetry
|
||||||
call:
|
call:
|
||||||
-
|
-
|
||||||
function: PersistUserEnvironmentConfiguration
|
function: PersistUserEnvironmentConfiguration
|
||||||
@@ -576,7 +576,7 @@ actions:
|
|||||||
name: Disable Siri voice feedback
|
name: Disable Siri voice feedback
|
||||||
recommend: strict
|
recommend: strict
|
||||||
docs:
|
docs:
|
||||||
- https://github.com/joeyhoer/starter/blob/master/system/siri.sh
|
- https://github.com/privacysexy-forks/starter/blob/master/system/siri.sh
|
||||||
- https://machippie.github.io/system/
|
- https://machippie.github.io/system/
|
||||||
code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3
|
code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3
|
||||||
revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2
|
revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2
|
||||||
@@ -955,7 +955,7 @@ actions:
|
|||||||
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no'
|
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no'
|
||||||
echo "Disabled Gatekeeper"
|
echo "Disabled Gatekeeper"
|
||||||
else
|
else
|
||||||
>&2 echo "Unknown gatekeeper status: $gatekeeper_status"
|
>&2 echo "Unknown gatekeeper status: $gatekeeper_status"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
revertCode: |-
|
revertCode: |-
|
||||||
@@ -974,7 +974,7 @@ actions:
|
|||||||
elif [ $gatekeeper_status = "enabled" ]; then
|
elif [ $gatekeeper_status = "enabled" ]; then
|
||||||
echo "No action needed, Gatekeeper is already enabled"
|
echo "No action needed, Gatekeeper is already enabled"
|
||||||
else
|
else
|
||||||
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
|
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,10 @@ import { IProjectInformation } from './IProjectInformation';
|
|||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
export class Application implements IApplication {
|
export class Application implements IApplication {
|
||||||
constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) {
|
constructor(
|
||||||
|
public info: IProjectInformation,
|
||||||
|
public collections: readonly ICategoryCollection[],
|
||||||
|
) {
|
||||||
validateInformation(info);
|
validateInformation(info);
|
||||||
validateCollections(collections);
|
validateCollections(collections);
|
||||||
}
|
}
|
||||||
@@ -37,8 +40,8 @@ function validateCollections(collections: readonly ICategoryCollection[]) {
|
|||||||
const osList = collections.map((c) => c.os);
|
const osList = collections.map((c) => c.os);
|
||||||
const duplicates = getDuplicates(osList);
|
const duplicates = getDuplicates(osList);
|
||||||
if (duplicates.length > 0) {
|
if (duplicates.length > 0) {
|
||||||
throw new Error('multiple collections with same os: ' +
|
throw new Error(`multiple collections with same os: ${
|
||||||
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "'));
|
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly documentationUrls: ReadonlyArray<string>,
|
public readonly documentationUrls: ReadonlyArray<string>,
|
||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>) {
|
public readonly scripts?: ReadonlyArray<IScript>,
|
||||||
|
) {
|
||||||
super(id);
|
super(id);
|
||||||
validateCategory(this);
|
validateCategory(this);
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,10 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getAllScriptsRecursively(): readonly IScript[] {
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
if (!this.allSubScripts) {
|
||||||
|
this.allSubScripts = parseScriptsRecursively(this);
|
||||||
|
}
|
||||||
|
return this.allSubScripts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +39,10 @@ function validateCategory(category: ICategory) {
|
|||||||
if (!category.name) {
|
if (!category.name) {
|
||||||
throw new Error('undefined or empty name');
|
throw new Error('undefined or empty name');
|
||||||
}
|
}
|
||||||
if ((!category.subCategories || category.subCategories.length === 0) &&
|
if (
|
||||||
(!category.scripts || category.scripts.length === 0)) {
|
(!category.subCategories || category.subCategories.length === 0)
|
||||||
|
&& (!category.scripts || category.scripts.length === 0)
|
||||||
|
) {
|
||||||
throw new Error('A category must have at least one sub-category or script');
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum';
|
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
||||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
@@ -9,6 +9,7 @@ import { ICategoryCollection } from './ICategoryCollection';
|
|||||||
|
|
||||||
export class CategoryCollection implements ICategoryCollection {
|
export class CategoryCollection implements ICategoryCollection {
|
||||||
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||||
|
|
||||||
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||||
|
|
||||||
private readonly queryable: IQueryableCollection;
|
private readonly queryable: IQueryableCollection;
|
||||||
@@ -16,7 +17,8 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly os: OperatingSystem,
|
public readonly os: OperatingSystem,
|
||||||
public readonly actions: ReadonlyArray<ICategory>,
|
public readonly actions: ReadonlyArray<ICategory>,
|
||||||
public readonly scripting: IScriptingDefinition) {
|
public readonly scripting: IScriptingDefinition,
|
||||||
|
) {
|
||||||
if (!scripting) {
|
if (!scripting) {
|
||||||
throw new Error('undefined scripting definition');
|
throw new Error('undefined scripting definition');
|
||||||
}
|
}
|
||||||
@@ -32,7 +34,7 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||||
if (isNaN(level)) {
|
if (level === undefined) {
|
||||||
throw new Error('undefined level');
|
throw new Error('undefined level');
|
||||||
}
|
}
|
||||||
if (!(level in RecommendationLevel)) {
|
if (!(level in RecommendationLevel)) {
|
||||||
@@ -55,20 +57,17 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||||
const totalOccurrencesById = new Map<TKey, number>();
|
const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array
|
||||||
for (const entity of entities) {
|
.findIndex((otherId) => otherId === id) !== index;
|
||||||
totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1);
|
const duplicatedIds = entities
|
||||||
}
|
.map((entity) => entity.id)
|
||||||
const duplicatedIds = new Array<TKey>();
|
.filter((id, index, array) => !isUniqueInArray(id, index, array))
|
||||||
totalOccurrencesById.forEach((index, id) => {
|
.filter(isUniqueInArray);
|
||||||
if (index > 1) {
|
|
||||||
duplicatedIds.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (duplicatedIds.length > 0) {
|
if (duplicatedIds.length > 0) {
|
||||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`);
|
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,50 +92,42 @@ function ensureValidScripts(allScripts: readonly IScript[]) {
|
|||||||
if (!allScripts || allScripts.length === 0) {
|
if (!allScripts || allScripts.length === 0) {
|
||||||
throw new Error('must consist of at least one script');
|
throw new Error('must consist of at least one script');
|
||||||
}
|
}
|
||||||
for (const level of getEnumValues(RecommendationLevel)) {
|
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||||
if (allScripts.every((script) => script.level !== level)) {
|
.filter((level) => allScripts.every((script) => script.level !== level));
|
||||||
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
|
if (missingRecommendationLevels.length > 0) {
|
||||||
}
|
throw new Error('none of the scripts are recommended as'
|
||||||
|
+ ` "${missingRecommendationLevels.map((level) => RecommendationLevel[level]).join(', "')}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
|
function flattenApplication(
|
||||||
const allCategories = new Array<ICategory>();
|
categories: ReadonlyArray<ICategory>,
|
||||||
const allScripts = new Array<IScript>();
|
): [ICategory[], IScript[]] {
|
||||||
flattenCategories(categories, allCategories, allScripts);
|
const [subCategories, subScripts] = (categories || [])
|
||||||
|
// Parse children
|
||||||
|
.map((category) => flattenApplication(category.subCategories))
|
||||||
|
// Flatten results
|
||||||
|
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
||||||
return [
|
return [
|
||||||
allCategories,
|
[...previousCategories, ...currentCategories],
|
||||||
allScripts,
|
[...previousScripts, ...currentScripts],
|
||||||
|
];
|
||||||
|
}, [new Array<ICategory>(), new Array<IScript>()]);
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
...(categories || []),
|
||||||
|
...subCategories,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...(categories || []).flatMap((category) => category.scripts || []),
|
||||||
|
...subScripts,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenCategories(
|
|
||||||
categories: ReadonlyArray<ICategory>,
|
|
||||||
allCategories: ICategory[],
|
|
||||||
allScripts: IScript[]): IQueryableCollection {
|
|
||||||
if (!categories || categories.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const category of categories) {
|
|
||||||
allCategories.push(category);
|
|
||||||
flattenScripts(category.scripts, allScripts);
|
|
||||||
flattenCategories(category.subCategories, allCategories, allScripts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenScripts(
|
|
||||||
scripts: ReadonlyArray<IScript>,
|
|
||||||
allScripts: IScript[]): IScript[] {
|
|
||||||
if (!scripts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scripts) {
|
|
||||||
allScripts.push(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeQueryable(
|
function makeQueryable(
|
||||||
actions: ReadonlyArray<ICategory>): IQueryableCollection {
|
actions: ReadonlyArray<ICategory>,
|
||||||
|
): IQueryableCollection {
|
||||||
const flattened = flattenApplication(actions);
|
const flattened = flattenApplication(actions);
|
||||||
return {
|
return {
|
||||||
allCategories: flattened[0],
|
allCategories: flattened[0],
|
||||||
@@ -145,12 +136,18 @@ function makeQueryable(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
|
function groupByLevel(
|
||||||
const map = new Map<RecommendationLevel, readonly IScript[]>();
|
allScripts: readonly IScript[],
|
||||||
for (const levelName of getEnumNames(RecommendationLevel)) {
|
): Map<RecommendationLevel, readonly IScript[]> {
|
||||||
const level = RecommendationLevel[levelName];
|
return getEnumValues(RecommendationLevel)
|
||||||
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
|
.map((level) => ({
|
||||||
map.set(level, scripts);
|
level,
|
||||||
}
|
scripts: allScripts.filter(
|
||||||
|
(script) => script.level !== undefined && script.level <= level,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.reduce((map, group) => {
|
||||||
|
map.set(group.level, group.scripts);
|
||||||
return map;
|
return map;
|
||||||
|
}, new Map<RecommendationLevel, readonly IScript[]>());
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user