Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
298b058e5c | ||
|
|
1e80ee1fb0 | ||
|
|
5901dc5f11 | ||
|
|
5721796378 | ||
|
|
8b374a37b4 | ||
|
|
c404dfebe2 | ||
|
|
e8199932b4 | ||
|
|
f4a7ca76b8 | ||
|
|
64cca1d9b8 | ||
|
|
68a5d698a2 | ||
|
|
bf0c55fa60 | ||
|
|
e7b816d156 | ||
|
|
a2e092190d | ||
|
|
c1c2f2925f | ||
|
|
e8d06e0f3e | ||
|
|
7d3670c26d | ||
|
|
430537f704 | ||
|
|
58ed7b456b | ||
|
|
6b3f4659df | ||
|
|
bbc6156281 | ||
|
|
df533ad3b1 | ||
|
|
6067bdb24e | ||
|
|
924b326244 | ||
|
|
8608072bfb | ||
|
|
3233d9b802 | ||
|
|
99e24b4134 | ||
|
|
b210aaddf2 | ||
|
|
65902e5b72 |
@@ -1,2 +1,3 @@
|
|||||||
> 1%
|
> 1%
|
||||||
last 2 versions
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
|||||||
1
.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
@@ -27,7 +27,14 @@ module.exports = {
|
|||||||
'@vue/typescript/recommended',
|
'@vue/typescript/recommended',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 12, // ECMA 2021
|
||||||
|
/*
|
||||||
|
Having 'latest' leads to:
|
||||||
|
```
|
||||||
|
Parsing error: ecmaVersion must be a number. Received value of type string instead
|
||||||
|
```
|
||||||
|
For .js files in the project
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...getOwnRules(),
|
...getOwnRules(),
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ A clear and concise description of what the bug is.
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which OS are you using? What version of OS you were using?
|
Which OS are you using? What version of OS you were using?
|
||||||
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
|
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||||
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||||
|
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Reproduction steps
|
### Reproduction steps
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ You could alternatively send a PR directly (see CONTRIBUTING.md).
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which OS will the new script configure?
|
Which OS will the new script configure?
|
||||||
Either "Windows" or "macOS".
|
One of the supported OSes: "Windows", "macOS" or "Linux".
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
@@ -31,9 +31,11 @@ E.g. "Disable webcam telemetry"
|
|||||||
Code that will be executed when script is selected.
|
Code that will be executed when script is selected.
|
||||||
Try to keep it as simple and backwards-compatible as possible.
|
Try to keep it as simple and backwards-compatible as possible.
|
||||||
Allowed languages:
|
Allowed languages:
|
||||||
- macOS: bash (sh)
|
|
||||||
- Windows: PowerShell (ps1) or batchfile
|
- Windows: PowerShell (ps1) or batchfile
|
||||||
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||||
|
- macOS: bash (sh)
|
||||||
|
- Linux: bash (sh) or Python 3
|
||||||
|
- 💡 Prioritize the one that's simpler, bash if similar.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Revert code
|
### Revert code
|
||||||
|
|||||||
20
.github/workflows/checks.build.yaml
vendored
@@ -53,3 +53,23 @@ jobs:
|
|||||||
run: |-
|
run: |-
|
||||||
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
||||||
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
||||||
|
|
||||||
|
create-icons:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
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: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Create icons
|
||||||
|
run: npm run create-icons
|
||||||
|
|||||||
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.4 (2022-03-08)
|
||||||
|
|
||||||
|
* Improve performance of selecting scripts | [8e96c19](https://github.com/undergroundwires/privacy.sexy/commit/8e96c19126aa4cba6418de5ccaa9e2dcf8faab78)
|
||||||
|
* Fix reverting of Windows NVIDIA telemetry service | [2354f0b](https://github.com/undergroundwires/privacy.sexy/commit/2354f0ba9fed3aa23569b5ea6391a7119fe1ab53)
|
||||||
|
* Add AirBnb TypeScript overrides for linting | [834ce8c](https://github.com/undergroundwires/privacy.sexy/commit/834ce8cf9e8e46934dfa604526360870d109765b)
|
||||||
|
* Transpile dependencies for wider browser support | [0e52a99](https://github.com/undergroundwires/privacy.sexy/commit/0e52a99efa2b02d1aba10885a76e03aa6f9be7f8)
|
||||||
|
* Add more and unify tests for absent object cases | [44d79e2](https://github.com/undergroundwires/privacy.sexy/commit/44d79e2c9a97639bbd188a8fdfd740f1a5a1d6ee)
|
||||||
|
* Fix Windows DoSvc not being disabled #115 | [43ce834](https://github.com/undergroundwires/privacy.sexy/commit/43ce834750ddf471636d1ece4324d02357947f9f)
|
||||||
|
* Move stubs from `./stubs` to `./shared/Stubs` | [803ef2b](https://github.com/undergroundwires/privacy.sexy/commit/803ef2bb3eea68306377e40e326c791402998650)
|
||||||
|
* Improve documentation for developing | [3c3ec80](https://github.com/undergroundwires/privacy.sexy/commit/3c3ec80525b97e8a24db4c44bbf42a7b4e089056)
|
||||||
|
* Improve documentation for architecture | [1bcc6c8](https://github.com/undergroundwires/privacy.sexy/commit/1bcc6c8b2b923b4d4b1662f990d86b190ce73342)
|
||||||
|
* Improve existing documentation | [db47440](https://github.com/undergroundwires/privacy.sexy/commit/db47440d470ea6a6e100b620b10d078c01314992)
|
||||||
|
* Refactor to remove code coupling with Webpack | [5bbbb9c](https://github.com/undergroundwires/privacy.sexy/commit/5bbbb9cecca0a3828036e7fc34dcd66970ce334a)
|
||||||
|
* Refactor to remove hardcoding of aliases | [481a02a](https://github.com/undergroundwires/privacy.sexy/commit/481a02afd5190eb77a37fa450e50816b2268e99c)
|
||||||
|
* Document WpnService breaking on Windows 10 #110 | [3785e41](https://github.com/undergroundwires/privacy.sexy/commit/3785e410db461f667a834e0b388d81e4baa028e4)
|
||||||
|
* Fix error when reverting Windows Defender setting | [956052c](https://github.com/undergroundwires/privacy.sexy/commit/956052c8fff042812fe84fe4d7fa5c579365ff9b)
|
||||||
|
* Fix Windows 11 being detected as Windows 10 | [d6bc33e](https://github.com/undergroundwires/privacy.sexy/commit/d6bc33ec865d50efc6b8d4ccc2f789edd874fcee)
|
||||||
|
* Refactor to use version object #59 | [eeb1d5b](https://github.com/undergroundwires/privacy.sexy/commit/eeb1d5b0c40a55675921af3f67f366b2ff658acf)
|
||||||
|
* Fix Microsoft Defender alert for uninstaller #114 | [112e79a](https://github.com/undergroundwires/privacy.sexy/commit/112e79a64c6153f4ce3b48c27a09639e7647aebc)
|
||||||
|
* Add donation information | [05a6a84](https://github.com/undergroundwires/privacy.sexy/commit/05a6a84c3739ec900343591ac1f7a9f310cd73f2)
|
||||||
|
* Bump node environment to 16.x | [242a497](https://github.com/undergroundwires/privacy.sexy/commit/242a497e7debb351da19b20b63a3554f0cca4b5c)
|
||||||
|
* Bump dependencies to latest | [efd63ff](https://github.com/undergroundwires/privacy.sexy/commit/efd63ff85dea4c9a9c033c54bc1be378742de351)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.3...0.11.4)
|
||||||
|
|
||||||
## 0.11.3 (2022-01-05)
|
## 0.11.3 (2022-01-05)
|
||||||
|
|
||||||
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy — Now you have the choice
|
||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
<!-- markdownlint-disable MD033 -->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -120,7 +120,7 @@ Online version does not require to run any software on your computer. Offline ve
|
|||||||
- **Reversible**. Revert if something feels wrong.
|
- **Reversible**. Revert if something feels wrong.
|
||||||
- **Accessible**. No need to run any compiled software on your computer with web version.
|
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||||
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
- **Tested.** A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||||
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
||||||
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# build
|
# build
|
||||||
|
|
||||||
- These are the file that are used by electron.
|
This folder contains files that are used by Electron to serve the desktop version.
|
||||||
- Logos are created by from the [PNG icon](./../public/icon.png)
|
|
||||||
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
14
cypress.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
fixturesFolder: 'tests/e2e/fixtures',
|
||||||
|
screenshotsFolder: 'tests/e2e/screenshots',
|
||||||
|
videosFolder: 'tests/e2e/videos',
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./tests/e2e/plugins/index.js')(on, config)
|
||||||
|
},
|
||||||
|
specPattern: 'tests/e2e/specs/**/*.cy.{js,jsx,ts,tsx}',
|
||||||
|
supportFile: 'tests/e2e/support/index.js',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"pluginsFile": "tests/e2e/plugins/index.js"
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||||
- 💡 Best practices
|
- 💡 Best practices
|
||||||
- If you repeat yourself, try to utilize [YAML-defined functions](#Function)
|
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
|
||||||
- Always try to add documentation and a way to revert a tweak in [scripts](#Script)
|
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
|
||||||
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||||
|
|
||||||
## Objects
|
## Objects
|
||||||
@@ -13,19 +13,19 @@
|
|||||||
- A collection simply defines:
|
- A collection simply defines:
|
||||||
- different categories and their scripts in a tree structure
|
- different categories and their scripts in a tree structure
|
||||||
- OS specific details
|
- OS specific details
|
||||||
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
|
- Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||||
|
|
||||||
#### `Collection` syntax
|
#### `Collection` syntax
|
||||||
|
|
||||||
- `os:` *`string`* (**required**)
|
- `os:` *`string`* (**required**)
|
||||||
- Operating system that the [Collection](#collection) is written for.
|
- Operating system that the [Collection](#collection) is written for.
|
||||||
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||||
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)**
|
||||||
- Each [category](#category) is rendered as different cards in card presentation.
|
- Each [category](#category) is rendered as different cards in card presentation.
|
||||||
- ❗ A [Collection](#collection) must consist of at least one category.
|
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||||
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
- `functions: [` ***[`Function`](#function)*** `, ... ]`
|
||||||
- Functions are optionally defined to re-use the same code throughout different scripts.
|
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||||
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
|
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||||
- Defines the scripting language that the code of other action uses.
|
- Defines the scripting language that the code of other action uses.
|
||||||
|
|
||||||
### `Category`
|
### `Category`
|
||||||
@@ -38,9 +38,12 @@
|
|||||||
- `category:` *`string`* (**required**)
|
- `category:` *`string`* (**required**)
|
||||||
- Name of the category
|
- Name of the category
|
||||||
- ❗ Must be unique throughout the [Collection](#collection)
|
- ❗ Must be unique throughout the [Collection](#collection)
|
||||||
- `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` (**required**)
|
||||||
- ❗ Category must consist of at least one subcategory or script.
|
- ❗ Category must consist of at least one subcategory or script.
|
||||||
- Children can be combination of scripts and subcategories.
|
- Children can be combination of scripts and subcategories.
|
||||||
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
|
- Documentation pieces related to the category.
|
||||||
|
- Rendered as markdown.
|
||||||
|
|
||||||
### `Script`
|
### `Script`
|
||||||
|
|
||||||
@@ -67,12 +70,12 @@
|
|||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
- ❗ Do not define if `call` is defined.
|
- ❗ Do not define if `call` is defined.
|
||||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||||
- A shared function or sequence of functions to call (called in order)
|
- A shared function or sequence of functions to call (called in order)
|
||||||
- ❗ If not defined `code` must be defined
|
- ❗ If not defined `code` must be defined
|
||||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
- Single documentation URL or list of URLs for those who wants to learn more about the script
|
- Documentation pieces related to the script.
|
||||||
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
|
- Rendered as markdown.
|
||||||
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||||
- If not defined then the script will not be recommended
|
- If not defined then the script will not be recommended
|
||||||
- If defined it can be either
|
- If defined it can be either
|
||||||
@@ -120,7 +123,7 @@
|
|||||||
- Convention is to use camelCase, and be verbs.
|
- Convention is to use camelCase, and be verbs.
|
||||||
- E.g. `uninstallStoreApp`
|
- E.g. `uninstallStoreApp`
|
||||||
- ❗ Function names must be unique
|
- ❗ Function names must be unique
|
||||||
- `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
|
- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]`
|
||||||
- List of parameters that function code refers to.
|
- List of parameters that function code refers to.
|
||||||
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||||
`code`: *`string`* (**required** if `call` is undefined)
|
`code`: *`string`* (**required** if `call` is undefined)
|
||||||
@@ -133,7 +136,7 @@
|
|||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||||
- A shared function or sequence of functions to call (called in order)
|
- A shared function or sequence of functions to call (called in order)
|
||||||
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
||||||
- ❗ If not defined `code` must be defined
|
- ❗ If not defined `code` must be defined
|
||||||
@@ -141,7 +144,7 @@
|
|||||||
### `FunctionParameter`
|
### `FunctionParameter`
|
||||||
|
|
||||||
- Defines a parameter that function requires optionally or mandatory.
|
- Defines a parameter that function requires optionally or mandatory.
|
||||||
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
|
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall).
|
||||||
|
|
||||||
#### `FunctionParameter` syntax
|
#### `FunctionParameter` syntax
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ You could run other types of tests as well, but they may take longer time and ov
|
|||||||
|
|
||||||
- Build web application: `npm run build`
|
- Build web application: `npm run build`
|
||||||
- Build desktop application: `npm run electron:build`
|
- Build desktop application: `npm run electron:build`
|
||||||
|
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
|
||||||
|
|
||||||
## Recommended extensions
|
## Recommended extensions
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ It's designed event-driven from bottom to top. It listens user events (from top)
|
|||||||
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations used by Vue CLI internally.
|
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations 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`.
|
||||||
|
|
||||||
|
## Visual design best-practices
|
||||||
|
|
||||||
|
Add visual clues for clickable items. It should be as clear as possible that they're interactable at first look without hovering. They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
||||||
|
|||||||
@@ -10,10 +10,19 @@
|
|||||||
|
|
||||||
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
||||||
- E.g. `Hello {{ $name }} !`
|
- E.g. `Hello {{ $name }} !`
|
||||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language.
|
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
||||||
- Functions enables usage of expressions.
|
- Functions enables usage of expressions.
|
||||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
||||||
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||||
|
- Expressions inside expressions (nested templates) are supported.
|
||||||
|
- An expression can output another expression that will also be compiled.
|
||||||
|
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $condition }}
|
||||||
|
echo {{ $text }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
### Parameter substitution
|
### Parameter substitution
|
||||||
|
|
||||||
@@ -56,9 +65,31 @@ A function can call other functions such as:
|
|||||||
|
|
||||||
### with
|
### with
|
||||||
|
|
||||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}`.
|
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
||||||
|
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
||||||
|
|
||||||
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
It supports multiline text inside the block. You can have something like:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $argument }}
|
||||||
|
First line
|
||||||
|
Second line
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $condition }}
|
||||||
|
This is a different parameter: {{ $text }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Common aspects for all tests:
|
|||||||
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
|
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
|
||||||
- Test names and folders have logical structure based on tests executed.
|
- Test names and folders have logical structure based on tests executed.
|
||||||
- The structure is following:
|
- The structure is following:
|
||||||
- [`cypress.json`](./../cypress.json): Cypress configuration file.
|
- [`cypress.config.ts`](./../cypress.config.ts): Cypress configuration file.
|
||||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
||||||
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
|
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
|
||||||
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.
|
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.
|
||||||
|
|||||||
95
fresh-npm-install.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Description:
|
||||||
|
# This script ensures npm is available, removes existing node modules, optionally
|
||||||
|
# removes package-lock.json (when -n flag is used), installs dependencies and runs unit tests.
|
||||||
|
# Usage:
|
||||||
|
# ./fresh-npm-install.sh # Regular execution
|
||||||
|
# ./fresh-npm-install.sh -n # Non-deterministic mode (removes package-lock.json)
|
||||||
|
|
||||||
|
declare NON_DETERMINISTIC_FLAG=0
|
||||||
|
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
ensure_npm_is_available
|
||||||
|
ensure_npm_root
|
||||||
|
remove_existing_modules
|
||||||
|
if [[ $NON_DETERMINISTIC_FLAG -eq 1 ]]; then
|
||||||
|
remove_package_lock_json
|
||||||
|
fi
|
||||||
|
install_dependencies
|
||||||
|
run_unit_tests
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_npm_is_available() {
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
log::fatal 'npm could not be found, please install it first.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_npm_root() {
|
||||||
|
if [ ! -f package.json ]; then
|
||||||
|
log::fatal 'Current directory is not a npm root. Please run the script in a npm root directory.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_existing_modules() {
|
||||||
|
if [ -d ./node_modules ]; then
|
||||||
|
log::info 'Removing existing node modules...'
|
||||||
|
if ! rm -rf ./node_modules; then
|
||||||
|
log::fatal 'Could not remove existing node modules.'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies() {
|
||||||
|
log::info 'Installing dependencies...'
|
||||||
|
if ! npm install; then
|
||||||
|
log::fatal 'Failed to install dependencies.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_package_lock_json() {
|
||||||
|
if [ -f ./package-lock.json ]; then
|
||||||
|
log::info 'Removing package-lock.json...'
|
||||||
|
if ! rm -rf ./package-lock.json; then
|
||||||
|
log::fatal 'Could not remove package-lock.json.'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_unit_tests() {
|
||||||
|
log::info 'Running unit tests...'
|
||||||
|
if ! npm run test:unit; then
|
||||||
|
pwd
|
||||||
|
log::fatal 'Failed to run unit tests.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info() {
|
||||||
|
local -r message="$1"
|
||||||
|
echo "📣 ${message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log::fatal() {
|
||||||
|
local -r message="$1"
|
||||||
|
echo "❌ ${message}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while getopts "n" opt; do
|
||||||
|
case ${opt} in
|
||||||
|
n)
|
||||||
|
NON_DETERMINISTIC_FLAG=1
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Invalid option: $OPTARG" 1>&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$1"
|
||||||
12
img/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# img
|
||||||
|
|
||||||
|
This folder contains image files and other resources related to images.
|
||||||
|
|
||||||
|
## logo.svg
|
||||||
|
|
||||||
|
[logo.svg](./logo.svg) is the master logo from which all other icons or images are created from.
|
||||||
|
It should be the only file that will be changed manually.
|
||||||
|
|
||||||
|
[`logo-update.mjs`](./logo-update.mjs) script in this folder updates all the logo files.
|
||||||
|
It should be executed everytime the logo is changed.
|
||||||
|
It automates recreation of logo files in different formats.
|
||||||
127
img/logo-update.mjs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
import { resolve, join } from 'path';
|
||||||
|
import { rm, mkdtemp, stat } from 'fs/promises';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { URL, fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
class Paths {
|
||||||
|
constructor(selfDirectory) {
|
||||||
|
const projectRoot = resolve(selfDirectory, '../');
|
||||||
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
|
this.publicDirectory = join(projectRoot, 'public');
|
||||||
|
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Source image: ${this.sourceImage}\n`
|
||||||
|
+ `Public directory: ${this.publicDirectory}\n`
|
||||||
|
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const paths = new Paths(getCurrentScriptDirectory());
|
||||||
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||||
|
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
||||||
|
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
||||||
|
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
||||||
|
console.log('🎉 (Re)created icons successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(publicFolder);
|
||||||
|
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
||||||
|
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'svgexport',
|
||||||
|
sourceImage,
|
||||||
|
electronTrayIconFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWebFavicon(sourceImage, faviconFolder) {
|
||||||
|
console.log('Updating favicon');
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(faviconFolder);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'icon-gen',
|
||||||
|
`--input ${sourceImage}`,
|
||||||
|
`--output ${faviconFolder}`,
|
||||||
|
'--ico',
|
||||||
|
'--ico-name \'favicon\'',
|
||||||
|
'--report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(electronIconsDir);
|
||||||
|
const temporaryDir = await mkdtemp('icon-');
|
||||||
|
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||||
|
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by icon-builder
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'svgexport',
|
||||||
|
sourceImage,
|
||||||
|
temporaryPngFile,
|
||||||
|
'1024:1024',
|
||||||
|
);
|
||||||
|
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'electron-icon-builder',
|
||||||
|
`--input="${temporaryPngFile}"`,
|
||||||
|
`--output="${electronIconsDir}"`,
|
||||||
|
'--flatten',
|
||||||
|
);
|
||||||
|
console.log('Cleaning up temporary directory.');
|
||||||
|
await rm(temporaryDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFileExists(filePath) {
|
||||||
|
const path = await stat(filePath);
|
||||||
|
if (!path.isFile()) {
|
||||||
|
throw new Error(`Not a file: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFolderExists(folderPath) {
|
||||||
|
const path = await stat(folderPath);
|
||||||
|
if (!path.isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${folderPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(...args) {
|
||||||
|
const command = args.join(' ');
|
||||||
|
console.log(`Running command: ${command}`);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const process = spawn(command, { shell: true });
|
||||||
|
process.stdout.on('data', (stdout) => {
|
||||||
|
console.log(stdout.toString());
|
||||||
|
});
|
||||||
|
process.stderr.on('data', (stderr) => {
|
||||||
|
console.error(stderr.toString());
|
||||||
|
});
|
||||||
|
process.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
process.on('close', (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
reject(new Error(`Process exited with non-zero exit code: ${exitCode}`));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
process.stdin.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentScriptDirectory() {
|
||||||
|
return fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
56
img/logo.svg
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="90.2998mm" height="90.2998mm"
|
||||||
|
viewBox="0 0 256 256">
|
||||||
|
<path id="logo"
|
||||||
|
fill="#3a65ab" stroke="#3a65ab" stroke-width="1"
|
||||||
|
d="M 128.00,173.00
|
||||||
|
C 128.00,173.00 102.00,175.00 102.00,175.00
|
||||||
|
85.39,174.97 64.02,170.31 49.00,163.22
|
||||||
|
38.46,158.24 28.39,152.17 20.01,143.96
|
||||||
|
14.88,138.93 10.72,133.32 7.31,127.00
|
||||||
|
3.36,119.66 1.10,112.37 1.00,104.00
|
||||||
|
1.00,104.00 1.00,98.00 1.00,98.00
|
||||||
|
1.29,73.92 24.76,53.44 44.00,42.43
|
||||||
|
84.66,19.15 129.75,23.12 169.00,47.00
|
||||||
|
188.74,59.02 207.93,76.23 208.00,101.00
|
||||||
|
208.00,101.00 188.00,101.00 188.00,101.00
|
||||||
|
186.46,86.48 168.72,71.84 157.00,64.61
|
||||||
|
140.76,54.59 121.33,46.78 102.00,47.00
|
||||||
|
88.42,47.16 72.20,52.52 60.00,58.32
|
||||||
|
45.30,65.30 19.83,84.81 21.10,103.00
|
||||||
|
21.39,107.16 22.92,110.33 24.76,114.00
|
||||||
|
32.70,129.78 48.16,140.02 64.00,146.72
|
||||||
|
75.16,151.44 92.90,155.26 105.00,154.99
|
||||||
|
113.45,154.79 121.81,152.84 130.00,151.00
|
||||||
|
130.00,151.00 128.00,173.00 128.00,173.00 Z
|
||||||
|
M 136.00,79.00
|
||||||
|
C 142.71,81.35 144.84,93.60 144.99,100.00
|
||||||
|
145.51,122.74 130.31,140.73 107.00,141.00
|
||||||
|
83.63,141.26 67.43,126.52 66.09,103.00
|
||||||
|
64.82,80.73 85.85,58.90 104.00,64.00
|
||||||
|
100.18,69.73 95.45,74.53 96.20,82.00
|
||||||
|
97.29,92.87 110.06,102.98 121.00,99.03
|
||||||
|
129.92,95.81 134.61,87.96 136.00,79.00 Z
|
||||||
|
M 186.00,113.46
|
||||||
|
C 206.11,110.69 225.57,114.92 239.91,130.01
|
||||||
|
252.85,143.63 255.21,157.09 255.00,175.00
|
||||||
|
254.76,195.49 241.26,214.25 223.00,222.88
|
||||||
|
213.06,227.58 204.72,228.12 194.00,228.00
|
||||||
|
150.34,227.49 126.71,178.85 146.32,142.00
|
||||||
|
154.93,125.82 168.55,117.23 186.00,113.46 Z
|
||||||
|
M 233.00,181.00
|
||||||
|
C 242.24,158.78 221.84,133.54 199.00,133.01
|
||||||
|
188.40,132.77 182.75,135.31 174.00,141.00
|
||||||
|
178.60,146.85 195.92,157.24 203.00,161.86
|
||||||
|
209.82,166.32 226.61,178.55 233.00,181.00 Z
|
||||||
|
M 221.00,200.00
|
||||||
|
C 216.39,194.15 206.42,188.61 200.00,184.33
|
||||||
|
192.31,179.21 168.77,162.59 162.00,160.00
|
||||||
|
159.67,165.03 159.94,166.57 160.00,172.00
|
||||||
|
160.23,190.99 177.11,207.55 196.00,207.99
|
||||||
|
206.60,208.23 212.25,205.69 221.00,200.00 Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
32877
package-lock.json
generated
117
package.json
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"slogan": "Now you have the choice",
|
||||||
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -10,84 +11,96 @@
|
|||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||||
|
"create-icons": "node img/logo-update.mjs",
|
||||||
"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:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||||
"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: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\""
|
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.3.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.0.0",
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.0.0",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.0.0",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.6",
|
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.4.14",
|
"ace-builds": "^1.23.4",
|
||||||
"core-js": "^3.21.1",
|
"core-js": "^3.32.0",
|
||||||
"cross-fetch": "^3.1.5",
|
"cross-fetch": "^4.0.0",
|
||||||
"electron-progressbar": "^2.0.1",
|
"electron-progressbar": "^2.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"npm": "^8.5.3",
|
"markdown-it": "^13.0.1",
|
||||||
|
"npm": "^9.8.1",
|
||||||
"v-tooltip": "2.1.3",
|
"v-tooltip": "2.1.3",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.7.14",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.1",
|
"vue-js-modal": "^2.0.1",
|
||||||
"vue-property-decorator": "^9.1.2"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "^0.0.48",
|
"@types/ace": "^0.0.48",
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.5",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@types/mocha": "^9.1.0",
|
"@types/mocha": "^10.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.13.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.1",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-e2e-cypress": "~5.0.1",
|
"@vue/cli-plugin-e2e-cypress": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.1",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-typescript": "~5.0.1",
|
"@vue/cli-plugin-typescript": "~5.0.8",
|
||||||
"@vue/cli-plugin-unit-mocha": "~5.0.1",
|
"@vue/cli-plugin-unit-mocha": "~5.0.8",
|
||||||
"@vue/cli-service": "~5.0.1",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-airbnb": "^6.0.0",
|
"@vue/eslint-config-airbnb": "^6.0.0",
|
||||||
"@vue/eslint-config-typescript": "^10.0.0",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/test-utils": "1.3.0",
|
"chai": "^4.3.7",
|
||||||
"chai": "^4.3.6",
|
"cypress": "^12.17.2",
|
||||||
"cypress": "^9.5.1",
|
"electron": "^25.3.2",
|
||||||
"electron": "^17.1.0",
|
"electron-builder": "^24.6.3",
|
||||||
"electron-builder": "^22.14.13",
|
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-log": "^4.4.6",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-updater": "^5.0.0",
|
"electron-log": "^4.4.8",
|
||||||
"eslint": "7.32.0",
|
"electron-updater": "^6.1.4",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint": "^8.46.0",
|
||||||
"eslint-plugin-vue": "^8.5.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^1.1.1",
|
"eslint-plugin-vue": "^9.6.0",
|
||||||
|
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||||
|
"icon-gen": "^3.0.1",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.31.1",
|
"markdownlint-cli": "^0.35.0",
|
||||||
"remark-cli": "^10.0.1",
|
"remark-cli": "^11.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^5.1.1",
|
"remark-preset-lint-consistent": "^5.1.2",
|
||||||
"remark-validate-links": "^11.0.2",
|
"remark-validate-links": "^12.1.1",
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.64.1",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^13.3.2",
|
||||||
"ts-loader": "9.0.1",
|
"svgexport": "^0.4.2",
|
||||||
"tslib": "^2.3.1",
|
"ts-loader": "^9.4.4",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "~4.6.2",
|
||||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
|
||||||
"vue-template-compiler": "^2.6.14",
|
"yaml-lint": "^1.7.0"
|
||||||
"yaml-lint": "^1.2.4"
|
},
|
||||||
|
"overrides": {
|
||||||
|
"vue-cli-plugin-electron-builder": {
|
||||||
|
"electron-builder": "^24.6.3"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4",
|
"typescript": [
|
||||||
"eslint": "Stuck at 7.32.0 because Vue CLI not supporting 8.x.x"
|
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
||||||
|
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
||||||
|
],
|
||||||
|
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
|
||||||
|
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
|
||||||
|
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
|
||||||
},
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 353 KiB |
BIN
public/icon.png
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
@@ -2,9 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title>
|
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows, macOS and Linux</title>
|
||||||
<meta name="robots" content="index,follow" />
|
<meta name="robots" content="index,follow" />
|
||||||
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
const NewLine = '\n';
|
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
export abstract class CodeBuilder implements ICodeBuilder {
|
export abstract class CodeBuilder implements ICodeBuilder {
|
||||||
@@ -59,10 +58,12 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.lines.join(NewLine);
|
return this.lines.join(this.getNewLineTerminator());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract getCommentDelimiter(): string;
|
protected abstract getCommentDelimiter(): string;
|
||||||
|
|
||||||
protected abstract writeStandardOut(text: string): string;
|
protected abstract writeStandardOut(text: string): string;
|
||||||
|
|
||||||
|
protected abstract getNewLineTerminator(): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export class BatchBuilder extends CodeBuilder {
|
|||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo ${escapeForEcho(text)}`;
|
return `echo ${escapeForEcho(text)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getNewLineTerminator(): string {
|
||||||
|
return '\r\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export class ShellBuilder extends CodeBuilder {
|
|||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo '${escapeForEcho(text)}'`;
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getNewLineTerminator(): string {
|
||||||
|
return '\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { 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';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
@@ -28,7 +29,7 @@ const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData [] = [
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
WindowsData, MacOsData,
|
WindowsData, MacOsData, LinuxData,
|
||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import type {
|
|||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||||
|
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||||
|
import { parseDocs } from './DocumentationParser';
|
||||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
|
||||||
@@ -12,35 +14,67 @@ let categoryIdCounter = 0;
|
|||||||
export function parseCategory(
|
export function parseCategory(
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
|
factory: CategoryFactoryType = CategoryFactory,
|
||||||
): Category {
|
): Category {
|
||||||
if (!context) { throw new Error('missing context'); }
|
if (!context) { throw new Error('missing context'); }
|
||||||
ensureValid(category);
|
return parseCategoryRecursively({
|
||||||
|
categoryData: category,
|
||||||
|
context,
|
||||||
|
factory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICategoryParseContext {
|
||||||
|
readonly categoryData: CategoryData,
|
||||||
|
readonly context: ICategoryCollectionParseContext,
|
||||||
|
readonly factory: CategoryFactoryType,
|
||||||
|
readonly parentCategory?: CategoryData,
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
||||||
|
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
subScripts: new Array<Script>(),
|
subScripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const data of category.children) {
|
for (const data of context.categoryData.children) {
|
||||||
parseCategoryChild(data, children, category, context);
|
parseNode({
|
||||||
|
nodeData: data,
|
||||||
|
children,
|
||||||
|
parent: context.categoryData,
|
||||||
|
factory: context.factory,
|
||||||
|
context: context.context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return context.factory(
|
||||||
|
/* id: */ categoryIdCounter++,
|
||||||
|
/* name: */ context.categoryData.category,
|
||||||
|
/* docs: */ parseDocs(context.categoryData),
|
||||||
|
/* categories: */ children.subCategories,
|
||||||
|
/* scripts: */ children.subScripts,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
new NodeValidator({
|
||||||
|
type: NodeType.Category,
|
||||||
|
selfNode: context.categoryData,
|
||||||
|
parentNode: context.parentCategory,
|
||||||
|
}).throw(err.message);
|
||||||
}
|
}
|
||||||
return new Category(
|
|
||||||
/* id: */ categoryIdCounter++,
|
|
||||||
/* name: */ category.category,
|
|
||||||
/* docs: */ parseDocUrls(category),
|
|
||||||
/* categories: */ children.subCategories,
|
|
||||||
/* scripts: */ children.subScripts,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValid(category: CategoryData) {
|
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
||||||
if (!category) {
|
new NodeValidator({
|
||||||
throw Error('missing category');
|
type: NodeType.Category,
|
||||||
}
|
selfNode: category,
|
||||||
if (!category.children || category.children.length === 0) {
|
parentNode: parentCategory,
|
||||||
throw Error(`category has no children: "${category.category}"`);
|
})
|
||||||
}
|
.assertDefined(category)
|
||||||
if (!category.category || category.category.length === 0) {
|
.assertValidName(category.category)
|
||||||
throw Error('category has no name');
|
.assert(
|
||||||
}
|
() => category.children && category.children.length > 0,
|
||||||
|
`"${category.category}" has no children.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategoryChildren {
|
interface ICategoryChildren {
|
||||||
@@ -48,22 +82,29 @@ interface ICategoryChildren {
|
|||||||
subScripts: Script[];
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
interface INodeParseContext {
|
||||||
data: CategoryOrScriptData,
|
readonly nodeData: CategoryOrScriptData;
|
||||||
children: ICategoryChildren,
|
readonly children: ICategoryChildren;
|
||||||
parent: CategoryData,
|
readonly parent: CategoryData;
|
||||||
context: ICategoryCollectionParseContext,
|
readonly factory: CategoryFactoryType;
|
||||||
) {
|
readonly context: ICategoryCollectionParseContext;
|
||||||
if (isCategory(data)) {
|
}
|
||||||
const subCategory = parseCategory(data as CategoryData, context);
|
function parseNode(context: INodeParseContext) {
|
||||||
children.subCategories.push(subCategory);
|
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
||||||
} else if (isScript(data)) {
|
validator.assertDefined(context.nodeData);
|
||||||
const scriptData = data as ScriptData;
|
if (isCategory(context.nodeData)) {
|
||||||
const script = parseScript(scriptData, context);
|
const subCategory = parseCategoryRecursively({
|
||||||
children.subScripts.push(script);
|
categoryData: context.nodeData as CategoryData,
|
||||||
|
context: context.context,
|
||||||
|
factory: context.factory,
|
||||||
|
parentCategory: context.parent,
|
||||||
|
});
|
||||||
|
context.children.subCategories.push(subCategory);
|
||||||
|
} else if (isScript(context.nodeData)) {
|
||||||
|
const script = parseScript(context.nodeData as ScriptData, context.context);
|
||||||
|
context.children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Child element is neither a category or a script.
|
validator.throw('Node is neither a category or a script.');
|
||||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +114,22 @@ function isScript(data: CategoryOrScriptData): data is ScriptData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
const { category } = data as CategoryData;
|
return hasProperty(data, 'category');
|
||||||
return category && category.length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(holder: InstructionHolder): boolean {
|
function hasCode(data: InstructionHolder): boolean {
|
||||||
return holder.code && holder.code.length > 0;
|
return hasProperty(data, 'code');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(holder: InstructionHolder) {
|
function hasCall(data: InstructionHolder) {
|
||||||
return holder.call !== undefined;
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasProperty(object: unknown, propertyName: string) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryFactoryType = (
|
||||||
|
...parameters: ConstructorParameters<typeof Category>) => Category;
|
||||||
|
|
||||||
|
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
||||||
|
|||||||
@@ -1,64 +1,58 @@
|
|||||||
import type { DocumentableData, DocumentationUrlsData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||||
if (!documentable) {
|
if (!documentable) {
|
||||||
throw new Error('missing documentable');
|
throw new Error('missing documentable');
|
||||||
}
|
}
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs || !docs.length) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let result = new DocumentationUrlContainer();
|
let result = new DocumentationContainer();
|
||||||
result = addDocs(docs, result);
|
result = addDocs(docs, result);
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(
|
function addDocs(
|
||||||
docs: DocumentationUrlsData,
|
docs: DocumentationData,
|
||||||
urls: DocumentationUrlContainer,
|
container: DocumentationContainer,
|
||||||
): DocumentationUrlContainer {
|
): DocumentationContainer {
|
||||||
if (docs instanceof Array) {
|
if (docs instanceof Array) {
|
||||||
urls.addUrls(docs);
|
if (docs.length > 0) {
|
||||||
|
container.addParts(docs);
|
||||||
|
}
|
||||||
} else if (typeof docs === 'string') {
|
} else if (typeof docs === 'string') {
|
||||||
urls.addUrl(docs);
|
container.addPart(docs);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
throwInvalidType();
|
||||||
}
|
}
|
||||||
return urls;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentationUrlContainer {
|
class DocumentationContainer {
|
||||||
private readonly urls = new Array<string>();
|
private readonly parts = new Array<string>();
|
||||||
|
|
||||||
public addUrl(url: string) {
|
public addPart(documentation: string) {
|
||||||
validateUrl(url);
|
if (!documentation) {
|
||||||
this.urls.push(url);
|
throw Error('missing documentation');
|
||||||
|
}
|
||||||
|
if (typeof documentation !== 'string') {
|
||||||
|
throwInvalidType();
|
||||||
|
}
|
||||||
|
this.parts.push(documentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUrls(urls: readonly string[]) {
|
public addParts(parts: readonly string[]) {
|
||||||
for (const url of urls) {
|
for (const part of parts) {
|
||||||
if (typeof url !== 'string') {
|
this.addPart(part);
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
|
||||||
}
|
|
||||||
this.addUrl(url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAll(): ReadonlyArray<string> {
|
public getAll(): ReadonlyArray<string> {
|
||||||
return this.urls;
|
return this.parts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUrl(docUrl: string): void {
|
function throwInvalidType() {
|
||||||
if (!docUrl) {
|
throw new Error('docs field (documentation) must be an array of strings');
|
||||||
throw new Error('Documentation url is null or empty');
|
|
||||||
}
|
|
||||||
if (docUrl.includes('\n')) {
|
|
||||||
throw new Error('Documentation url cannot be multi-lined.');
|
|
||||||
}
|
|
||||||
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
|
||||||
const res = docUrl.match(validUrlRegex);
|
|
||||||
if (res == null) {
|
|
||||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/application/Parser/NodeValidation/NodeData.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { ScriptData, CategoryData } from '@/application/collections/';
|
||||||
|
|
||||||
|
export type NodeData = CategoryData | ScriptData;
|
||||||
35
src/application/Parser/NodeValidation/NodeDataError.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { NodeType } from './NodeType';
|
||||||
|
import { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export class NodeDataError extends Error {
|
||||||
|
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
||||||
|
super(createMessage(message, context));
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype); // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||||
|
this.name = new.target.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeDataErrorContext {
|
||||||
|
readonly type?: NodeType;
|
||||||
|
readonly selfNode: NodeData;
|
||||||
|
readonly parentNode?: NodeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
|
||||||
|
let message = '';
|
||||||
|
if (context.type !== undefined) {
|
||||||
|
message += `${NodeType[context.type]}: `;
|
||||||
|
}
|
||||||
|
message += errorMessage;
|
||||||
|
message += `\n${dump(context)}`;
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dump(context: INodeDataErrorContext): string {
|
||||||
|
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
|
||||||
|
let output = `Self: ${printJson(context.selfNode)}`;
|
||||||
|
if (context.parentNode) {
|
||||||
|
output += `\nParent: ${printJson(context.parentNode)}`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
4
src/application/Parser/NodeValidation/NodeType.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum NodeType {
|
||||||
|
Script,
|
||||||
|
Category,
|
||||||
|
}
|
||||||
38
src/application/Parser/NodeValidation/NodeValidator.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
||||||
|
import { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export class NodeValidator {
|
||||||
|
constructor(private readonly context: INodeDataErrorContext) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertValidName(nameValue: string) {
|
||||||
|
return this
|
||||||
|
.assert(
|
||||||
|
() => Boolean(nameValue),
|
||||||
|
'missing name',
|
||||||
|
)
|
||||||
|
.assert(
|
||||||
|
() => typeof nameValue === 'string',
|
||||||
|
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertDefined(node: NodeData) {
|
||||||
|
return this.assert(
|
||||||
|
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||||
|
'missing node data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assert(validationPredicate: () => boolean, errorMessage: string) {
|
||||||
|
if (!validationPredicate()) {
|
||||||
|
this.throw(errorMessage);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public throw(errorMessage: string) {
|
||||||
|
throw new NodeDataError(errorMessage, this.context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,26 @@ import { ProjectInformation } from '@/domain/ProjectInformation';
|
|||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
|
|
||||||
export function parseProjectInformation(
|
export function parseProjectInformation(
|
||||||
environment: NodeJS.ProcessEnv,
|
environment: NodeJS.ProcessEnv | VueAppEnvironment,
|
||||||
): IProjectInformation {
|
): IProjectInformation {
|
||||||
const version = new Version(environment.VUE_APP_VERSION);
|
const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]);
|
||||||
return new ProjectInformation(
|
return new ProjectInformation(
|
||||||
environment.VUE_APP_NAME,
|
environment[VueAppEnvironmentKeys.VUE_APP_NAME],
|
||||||
version,
|
version,
|
||||||
environment.VUE_APP_REPOSITORY_URL,
|
environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN],
|
||||||
environment.VUE_APP_HOMEPAGE_URL,
|
environment[VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL],
|
||||||
|
environment[VueAppEnvironmentKeys.VUE_APP_HOMEPAGE_URL],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const VueAppEnvironmentKeys = {
|
||||||
|
VUE_APP_VERSION: 'VUE_APP_VERSION',
|
||||||
|
VUE_APP_NAME: 'VUE_APP_NAME',
|
||||||
|
VUE_APP_SLOGAN: 'VUE_APP_SLOGAN',
|
||||||
|
VUE_APP_REPOSITORY_URL: 'VUE_APP_REPOSITORY_URL',
|
||||||
|
VUE_APP_HOMEPAGE_URL: 'VUE_APP_HOMEPAGE_URL',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type VueAppEnvironment = {
|
||||||
|
[K in keyof typeof VueAppEnvironmentKeys]: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
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';
|
||||||
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
||||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
||||||
|
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
public readonly compiler: IScriptCompiler;
|
public readonly compiler: IScriptCompiler;
|
||||||
|
|||||||
@@ -13,4 +13,22 @@ export class ExpressionPosition {
|
|||||||
throw Error(`negative start position: ${start}`);
|
throw Error(`negative start position: ${start}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isInInsideOf(potentialParent: ExpressionPosition): boolean {
|
||||||
|
if (this.isSame(potentialParent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return potentialParent.start <= this.start
|
||||||
|
&& potentialParent.end >= this.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSame(other: ExpressionPosition): boolean {
|
||||||
|
return other.start === this.start
|
||||||
|
&& other.end === this.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isIntersecting(other: ExpressionPosition): boolean {
|
||||||
|
return (other.start < this.end && other.end > this.start)
|
||||||
|
|| (this.end > other.start && other.start >= this.start);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,21 +20,59 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
const expressions = this.extractor.findExpressions(code);
|
|
||||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
|
||||||
const context = new ExpressionEvaluationContext(args);
|
const context = new ExpressionEvaluationContext(args);
|
||||||
const compiledCode = compileExpressions(expressions, code, context);
|
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||||
return compiledCode;
|
return compiledCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compileRecursively(
|
||||||
|
code: string,
|
||||||
|
context: IExpressionEvaluationContext,
|
||||||
|
extractor: IExpressionParser,
|
||||||
|
): string {
|
||||||
|
/*
|
||||||
|
Instead of compiling code at once and returning we compile expressions from the code.
|
||||||
|
And recompile expressions from resulting code recursively.
|
||||||
|
This allows using expressions inside expressions blocks. E.g.:
|
||||||
|
```
|
||||||
|
{{ with $condition }}
|
||||||
|
echo '{{ $text }}'
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
Without recursing parameter substitution for '{{ $text }}' is skipped once the outer
|
||||||
|
{{ with $condition }} is rendered.
|
||||||
|
A more optimized alternative to recursion would be to a parse an expression tree
|
||||||
|
instead of linear expression lists.
|
||||||
|
*/
|
||||||
|
if (!code) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
const expressions = extractor.findExpressions(code);
|
||||||
|
if (expressions.length === 0) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
const compiledCode = compileExpressions(expressions, code, context);
|
||||||
|
return compileRecursively(compiledCode, context, extractor);
|
||||||
|
}
|
||||||
|
|
||||||
function compileExpressions(
|
function compileExpressions(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
code: string,
|
code: string,
|
||||||
context: IExpressionEvaluationContext,
|
context: IExpressionEvaluationContext,
|
||||||
) {
|
) {
|
||||||
|
ensureValidExpressions(expressions, code, context);
|
||||||
let compiledCode = '';
|
let compiledCode = '';
|
||||||
const sortedExpressions = expressions
|
const outerExpressions = expressions.filter(
|
||||||
|
(expression) => expressions
|
||||||
|
.filter((otherExpression) => otherExpression !== expression)
|
||||||
|
.every((otherExpression) => !expression.position.isInInsideOf(otherExpression.position)),
|
||||||
|
);
|
||||||
|
/*
|
||||||
|
This logic will only compile outer expressions if there were nested expressions.
|
||||||
|
So the output of this compilation may result in new uncompiled expressions.
|
||||||
|
*/
|
||||||
|
const sortedExpressions = outerExpressions
|
||||||
.slice() // copy the array to not mutate the parameter
|
.slice() // copy the array to not mutate the parameter
|
||||||
.sort((a, b) => b.position.start - a.position.start);
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
@@ -65,6 +103,43 @@ function extractRequiredParameterNames(
|
|||||||
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('", "')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidExpressions(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
code: string,
|
||||||
|
context: IExpressionEvaluationContext,
|
||||||
|
) {
|
||||||
|
ensureParamsUsedInCodeHasArgsProvided(expressions, context.args);
|
||||||
|
ensureExpressionsDoesNotExtendCodeLength(expressions, code);
|
||||||
|
ensureNoExpressionsAtSamePosition(expressions);
|
||||||
|
ensureNoInvalidIntersections(expressions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureExpressionsDoesNotExtendCodeLength(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
code: string,
|
||||||
|
) {
|
||||||
|
const expectedMax = code.length;
|
||||||
|
const expressionsOutOfRange = expressions
|
||||||
|
.filter((expression) => expression.position.end > expectedMax);
|
||||||
|
if (expressionsOutOfRange.length > 0) {
|
||||||
|
throw new Error(`Expressions out of range:\n${JSON.stringify(expressionsOutOfRange)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoExpressionsAtSamePosition(expressions: readonly IExpression[]) {
|
||||||
|
const instructionsAtSamePosition = expressions.filter(
|
||||||
|
(expression) => expressions
|
||||||
|
.filter((other) => expression.position.isSame(other.position)).length > 1,
|
||||||
|
);
|
||||||
|
if (instructionsAtSamePosition.length > 0) {
|
||||||
|
throw new Error(`Instructions at same position:\n${JSON.stringify(instructionsAtSamePosition)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureParamsUsedInCodeHasArgsProvided(
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
@@ -80,6 +155,16 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
function ensureNoInvalidIntersections(expressions: readonly IExpression[]) {
|
||||||
return `"${list.join('", "')}"`;
|
const intersectingInstructions = expressions.filter(
|
||||||
|
(expression) => expressions
|
||||||
|
.filter((other) => expression.position.isIntersecting(other.position))
|
||||||
|
.filter((other) => !expression.position.isSame(other.position))
|
||||||
|
.filter((other) => !expression.position.isInInsideOf(other.position))
|
||||||
|
.filter((other) => !other.position.isInInsideOf(expression.position))
|
||||||
|
.length > 0,
|
||||||
|
);
|
||||||
|
if (intersectingInstructions.length > 0) {
|
||||||
|
throw new Error(`Instructions intersecting unexpectedly:\n${JSON.stringify(intersectingInstructions)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
|
|||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchAnythingExceptSurroundingWhitespaces() {
|
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('(.+?)')
|
.addRawRegex('([\\S\\s]+?)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
|
|||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
// ...
|
// ...
|
||||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
// {{ end }}
|
// {{ end }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('end')
|
.expectCharacters('end')
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function compileCode(
|
|||||||
compiler: IExpressionsCompiler,
|
compiler: IExpressionsCompiler,
|
||||||
): ICompiledFunctionCall {
|
): ICompiledFunctionCall {
|
||||||
return {
|
return {
|
||||||
code: compiler.compileExpressions(code.do, args),
|
code: compiler.compileExpressions(code.execute, args),
|
||||||
revertCode: compiler.compileExpressions(code.revert, args),
|
revertCode: compiler.compileExpressions(code.revert, args),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export enum FunctionBodyType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IFunctionCode {
|
export interface IFunctionCode {
|
||||||
readonly do: string;
|
readonly execute: string;
|
||||||
readonly revert?: string;
|
readonly revert?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
export interface ISharedFunctionsParser {
|
export interface ISharedFunctionsParser {
|
||||||
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
parseFunctions(
|
||||||
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
): ISharedFunctionCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IFunctionCall } from './Call/IFunctionCall';
|
import { IFunctionCall } from './Call/IFunctionCall';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||||
} from './ISharedFunction';
|
} from './ISharedFunction';
|
||||||
@@ -25,7 +26,7 @@ export function createFunctionWithInlineCode(
|
|||||||
throw new Error(`undefined code in function "${name}"`);
|
throw new Error(`undefined code in function "${name}"`);
|
||||||
}
|
}
|
||||||
const content: IFunctionCode = {
|
const content: IFunctionCode = {
|
||||||
do: code,
|
execute: code,
|
||||||
revert: revertCode,
|
revert: revertCode,
|
||||||
};
|
};
|
||||||
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
@@ -12,16 +17,20 @@ 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();
|
||||||
|
|
||||||
|
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
|
||||||
|
|
||||||
public parseFunctions(
|
public parseFunctions(
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
): ISharedFunctionCollection {
|
): ISharedFunctionCollection {
|
||||||
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
if (!functions || !functions.length) {
|
if (!functions || !functions.length) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
return functions
|
return functions
|
||||||
.map((func) => parseFunction(func))
|
.map((func) => parseFunction(func, syntax, this.codeValidator))
|
||||||
.reduce((acc, func) => {
|
.reduce((acc, func) => {
|
||||||
acc.addFunction(func);
|
acc.addFunction(func);
|
||||||
return acc;
|
return acc;
|
||||||
@@ -29,10 +38,15 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunction(data: FunctionData): ISharedFunction {
|
function parseFunction(
|
||||||
|
data: FunctionData,
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
validator: ICodeValidator,
|
||||||
|
): ISharedFunction {
|
||||||
const { name } = data;
|
const { name } = data;
|
||||||
const parameters = parseParameters(data);
|
const parameters = parseParameters(data);
|
||||||
if (hasCode(data)) {
|
if (hasCode(data)) {
|
||||||
|
validateCode(data, syntax, validator);
|
||||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
}
|
}
|
||||||
// Has call
|
// Has call
|
||||||
@@ -40,6 +54,19 @@ function parseFunction(data: FunctionData): ISharedFunction {
|
|||||||
return createCallerFunction(name, parameters, calls);
|
return createCallerFunction(name, parameters, calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCode(
|
||||||
|
data: FunctionData,
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
validator: ICodeValidator,
|
||||||
|
): void {
|
||||||
|
[data.code, data.revertCode].forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
return (data.parameters || [])
|
return (data.parameters || [])
|
||||||
.map((parameter) => {
|
.map((parameter) => {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
import type { FunctionData, ScriptData } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
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';
|
||||||
@@ -8,18 +12,20 @@ import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompi
|
|||||||
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
||||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||||
|
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
|
||||||
|
|
||||||
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,
|
syntax: ILanguageSyntax,
|
||||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
|
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||||
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
@@ -35,13 +41,19 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
try {
|
try {
|
||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||||
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
return new ScriptCode(
|
return new ScriptCode(
|
||||||
compiledCode.code,
|
compiledCode.code,
|
||||||
compiledCode.revertCode,
|
compiledCode.revertCode,
|
||||||
this.syntax,
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw Error(`Script "${script.name}" ${error.message}`);
|
throw Error(`Script "${script.name}" ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
|
||||||
|
[compiledCode.code, compiledCode.revertCode].forEach(
|
||||||
|
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export interface ICategoryCollectionParseContext {
|
export interface ICategoryCollectionParseContext {
|
||||||
readonly compiler: IScriptCompiler;
|
readonly compiler: IScriptCompiler;
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData } from '@/application/collections/';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { Script } from '@/domain/Script';
|
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 { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import { parseDocs } from '../DocumentationParser';
|
||||||
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||||
|
import { NodeType } from '../NodeValidation/NodeType';
|
||||||
|
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
data: ScriptData,
|
data: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
levelParser = createEnumParser(RecommendationLevel),
|
levelParser = createEnumParser(RecommendationLevel),
|
||||||
|
scriptFactory: ScriptFactoryType = ScriptFactory,
|
||||||
|
codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
): Script {
|
): Script {
|
||||||
validateScript(data);
|
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||||
|
validateScript(data, validator);
|
||||||
if (!context) { throw new Error('missing context'); }
|
if (!context) { throw new Error('missing context'); }
|
||||||
const script = new Script(
|
try {
|
||||||
/* name: */ data.name,
|
const script = scriptFactory(
|
||||||
/* code: */ parseCode(data, context),
|
/* name: */ data.name,
|
||||||
/* docs: */ parseDocUrls(data),
|
/* code: */ parseCode(data, context, codeValidator),
|
||||||
/* level: */ parseLevel(data.recommend, levelParser),
|
/* docs: */ parseDocs(data),
|
||||||
);
|
/* level: */ parseLevel(data.recommend, levelParser),
|
||||||
return script;
|
);
|
||||||
|
return script;
|
||||||
|
} catch (err) {
|
||||||
|
validator.throw(err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
@@ -33,28 +48,50 @@ function parseLevel(
|
|||||||
return parser.parseEnum(level, 'level');
|
return parser.parseEnum(level, 'level');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
|
function parseCode(
|
||||||
|
script: ScriptData,
|
||||||
|
context: ICategoryCollectionParseContext,
|
||||||
|
codeValidator: ICodeValidator,
|
||||||
|
): IScriptCode {
|
||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
const code = new ScriptCode(script.code, script.revertCode);
|
||||||
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
function validateHardcodedCodeWithoutCalls(
|
||||||
if (script.code && script.call) {
|
scriptCode: ScriptCode,
|
||||||
throw new Error('cannot define both "call" and "code"');
|
codeValidator: ICodeValidator,
|
||||||
}
|
syntax: ILanguageSyntax,
|
||||||
if (script.revertCode && script.call) {
|
) {
|
||||||
throw new Error('cannot define "revertCode" if "call" is defined');
|
[scriptCode.execute, scriptCode.revert].forEach(
|
||||||
}
|
(code) => codeValidator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateScript(script: ScriptData) {
|
function validateScript(script: ScriptData, validator: NodeValidator) {
|
||||||
if (!script) {
|
validator
|
||||||
throw new Error('missing script');
|
.assertDefined(script)
|
||||||
}
|
.assertValidName(script.name)
|
||||||
if (!script.code && !script.call) {
|
.assert(
|
||||||
throw new Error('must define either "call" or "code"');
|
() => Boolean(script.code || script.call),
|
||||||
}
|
'Must define either "call" or "code".',
|
||||||
ensureNotBothCallAndCode(script);
|
)
|
||||||
|
.assert(
|
||||||
|
() => !(script.code && script.call),
|
||||||
|
'Cannot define both "call" and "code".',
|
||||||
|
)
|
||||||
|
.assert(
|
||||||
|
() => !(script.revertCode && script.call),
|
||||||
|
'Cannot define "revertCode" if "call" is defined.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ScriptFactoryType = (...parameters: ConstructorParameters<typeof Script>) => Script;
|
||||||
|
|
||||||
|
const ScriptFactory: ScriptFactoryType = (...parameters) => new Script(...parameters);
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
|
|
||||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
|
||||||
public readonly commentDelimiters = ['#'];
|
|
||||||
|
|
||||||
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
|
|
||||||
}
|
|
||||||
46
src/application/Parser/Script/Validation/CodeValidator.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule';
|
||||||
|
import { ICodeValidator } from './ICodeValidator';
|
||||||
|
import { ICodeLine } from './ICodeLine';
|
||||||
|
|
||||||
|
export class CodeValidator implements ICodeValidator {
|
||||||
|
public static readonly instance: ICodeValidator = new CodeValidator();
|
||||||
|
|
||||||
|
public throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void {
|
||||||
|
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
|
||||||
|
if (!code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = extractLines(code);
|
||||||
|
const invalidLines = rules.flatMap((rule) => rule.analyze(lines));
|
||||||
|
if (invalidLines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const errorText = `Errors with the code.\n${printLines(lines, invalidLines)}`;
|
||||||
|
throw new Error(errorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLines(code: string): ICodeLine[] {
|
||||||
|
return code
|
||||||
|
.split(/\r\n|\r|\n/)
|
||||||
|
.map((lineText, lineIndex): ICodeLine => ({
|
||||||
|
index: lineIndex + 1,
|
||||||
|
text: lineText,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function printLines(
|
||||||
|
lines: readonly ICodeLine[],
|
||||||
|
invalidLines: readonly IInvalidCodeLine[],
|
||||||
|
): string {
|
||||||
|
return lines.map((line) => {
|
||||||
|
const badLine = invalidLines.find((invalidLine) => invalidLine.index === line.index);
|
||||||
|
if (!badLine) {
|
||||||
|
return `[${line.index}] ✅ ${line.text}`;
|
||||||
|
}
|
||||||
|
return `[${badLine.index}] ❌ ${line.text}\n\t⟶ ${badLine.error}`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
4
src/application/Parser/Script/Validation/ICodeLine.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ICodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly text: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ICodeLine } from './ICodeLine';
|
||||||
|
|
||||||
|
export interface IInvalidCodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICodeValidationRule {
|
||||||
|
analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ICodeValidationRule } from './ICodeValidationRule';
|
||||||
|
|
||||||
|
export interface ICodeValidator {
|
||||||
|
throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { ICodeLine } from '../ICodeLine';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
|
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||||
|
constructor(private readonly syntax: ILanguageSyntax) {
|
||||||
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.map((line): IDuplicateAnalyzedLine => ({
|
||||||
|
index: line.index,
|
||||||
|
isIgnored: shouldIgnoreLine(line.text, this.syntax),
|
||||||
|
occurrenceIndices: lines
|
||||||
|
.filter((other) => other.text === line.text)
|
||||||
|
.map((duplicatedLine) => duplicatedLine.index),
|
||||||
|
}))
|
||||||
|
.filter((line) => hasInvalidDuplicates(line))
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: `Line is duplicated at line numbers ${line.occurrenceIndices.join(',')}.`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDuplicateAnalyzedLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly occurrenceIndices: readonly number[];
|
||||||
|
readonly isIgnored: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInvalidDuplicates(line: IDuplicateAnalyzedLine): boolean {
|
||||||
|
return !line.isIgnored && line.occurrenceIndices.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
|
const lowerCaseCodeLine = codeLine.toLowerCase();
|
||||||
|
const isCommentLine = () => syntax.commentDelimiters.some(
|
||||||
|
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
||||||
|
);
|
||||||
|
const consistsOfFrequentCommands = () => {
|
||||||
|
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
||||||
|
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||||
|
};
|
||||||
|
return isCommentLine() || consistsOfFrequentCommands();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ICodeLine } from '../ICodeLine';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
|
export class NoEmptyLines implements ICodeValidationRule {
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.filter((line) => (line.text?.trim().length ?? 0) === 0)
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: (() => {
|
||||||
|
if (!line.text) {
|
||||||
|
return 'Empty line';
|
||||||
|
}
|
||||||
|
const markedText = line.text
|
||||||
|
.replaceAll(' ', '{whitespace}')
|
||||||
|
.replaceAll('\t', '{tab}');
|
||||||
|
return `Empty line: "${markedText}"`;
|
||||||
|
})(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||||
const PowerShellCommonCodeParts = ['{', '}'];
|
const PowerShellCommonCodeParts = ['{', '}'];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ILanguageSyntax {
|
||||||
|
readonly commentDelimiters: string[];
|
||||||
|
readonly commonCodeParts: string[];
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
import { ILanguageSyntax } from './ILanguageSyntax';
|
||||||
|
|
||||||
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
|
public readonly commentDelimiters = ['#'];
|
||||||
|
|
||||||
|
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
@@ -12,10 +12,10 @@ declare module '@/application/collections/*' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||||
export type DocumentationUrlsData = ReadonlyArray<string> | string;
|
export type DocumentationData = ReadonlyArray<string> | string;
|
||||||
|
|
||||||
export interface DocumentableData {
|
export interface DocumentableData {
|
||||||
readonly docs?: DocumentationUrlsData;
|
readonly docs?: DocumentationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstructionHolder {
|
export interface InstructionHolder {
|
||||||
|
|||||||
3690
src/application/collections/linux.yaml
Normal file
@@ -509,6 +509,83 @@ actions:
|
|||||||
function: PersistUserEnvironmentConfiguration
|
function: PersistUserEnvironmentConfiguration
|
||||||
parameters:
|
parameters:
|
||||||
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
||||||
|
-
|
||||||
|
category: Configure Parallels Desktop
|
||||||
|
docs: |-
|
||||||
|
Parallels Desktop for Mac is software providing hardware virtualization for macOS [1].
|
||||||
|
|
||||||
|
When you use it, it collects and share your personal data to third parties [2]. Personal
|
||||||
|
data include IP address of your device, your broad geographical location (country, state
|
||||||
|
(if applicable), and city) and used product [2].
|
||||||
|
|
||||||
|
It includes third-party ads [3] and automatic check for updates [4] by default. Both of these
|
||||||
|
behaviors communicate with online services that reveal data about you.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012155943/https://en.wikipedia.org/wiki/Parallels_Desktop_for_Mac "Parallels Desktop for Mac - Wikipedia | en.wikipedia.org"
|
||||||
|
[2]: https://web.archive.org/web/20221012155829/https://www.parallels.com/about/legal/privacy/ "Privacy Statement | parallels.com"
|
||||||
|
[3]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
|
||||||
|
[4]: https://web.archive.org/web/20221012151953/http://download.parallels.com/stm/docs/en/Parallels_Desktop_Users_Guide/22220.htm "Automatic Updating | Parallels Desktop Users Guide | download.parallels.com"
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Turn off ads in Parallels Desktop
|
||||||
|
recommend: standard
|
||||||
|
docs: |-
|
||||||
|
Parallels Desktop in-product notifications to show ads from Parallels or other third
|
||||||
|
party companies [1].
|
||||||
|
|
||||||
|
The main setting is `ProductPromo.ForcePromoOff` [1] that you can check using:
|
||||||
|
|
||||||
|
1. `defaults read 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff'`
|
||||||
|
2. `defaults read 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff'`
|
||||||
|
|
||||||
|
By default, on clean installations the value is `0` which is equivalent of `no`.
|
||||||
|
|
||||||
|
There is also `WelcomeScreenPromo.PromoOff` setting that's pre-configured to `1` (`no` as
|
||||||
|
default). It's undocumented but still kept disabled by this script.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/save/https://forum.parallels.com/threads/unable-to-process-the-upgrade-request.345603/ "Unable to process the upgrade request | Parallels Forums | forum.parallels.com"
|
||||||
|
[2]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
|
||||||
|
code: |-
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff' -bool yes
|
||||||
|
revertCode: |-
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool no
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff' -bool yes
|
||||||
|
-
|
||||||
|
category: Disable Parallels Desktop auto-updates
|
||||||
|
docs: |-
|
||||||
|
Parallels Desktop by default checks for updates frequently and automatically downloads them [1].
|
||||||
|
This reveal personal data about [2] you without your control.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012151953/http://download.parallels.com/stm/docs/en/Parallels_Desktop_Users_Guide/22220.htm "Automatic Updating | Parallels Desktop Users Guide | download.parallels.com"
|
||||||
|
[2]: https://web.archive.org/web/20221012155829/https://www.parallels.com/about/legal/privacy/ "Privacy Statement | parallels.com"
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable automatically downloading Parallels Desktop updates
|
||||||
|
docs: |-
|
||||||
|
Automatic downloads are enabled by default, and this script disables automatic downloads.
|
||||||
|
|
||||||
|
Automatic downloads are configured using the `Application preferences.Download updates automatically` property [1].
|
||||||
|
|
||||||
|
- Check: `defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically'`
|
||||||
|
- Values: 0 - Disabled, 1 - Enabled (default)
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012153810/https://download.parallels.com/desktop/v18/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm "Parallels Desktop Business Edition Administrator's Guide v18 - Configuring individual Macs | download.parallels.com"
|
||||||
|
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool no
|
||||||
|
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool yes
|
||||||
|
-
|
||||||
|
name: Disable automatically checking for Parallels Desktop updates
|
||||||
|
docs: |-
|
||||||
|
Automatic checks are weekly by default, and this script disables the checks completely.
|
||||||
|
|
||||||
|
Frequency to check for updates can be configured using `Application preferences.Check for updates` property [1].
|
||||||
|
|
||||||
|
- Check: `defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates'`
|
||||||
|
- Values: 0 - Never, 1 - Once a day, 2 - Once a week (default), 3 - Once a month
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012153810/https://download.parallels.com/desktop/v18/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm "Parallels Desktop Business Edition Administrator's Guide v18 - Configuring individual Macs | download.parallels.com"
|
||||||
|
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 0
|
||||||
|
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 2
|
||||||
-
|
-
|
||||||
category: Configure OS
|
category: Configure OS
|
||||||
children:
|
children:
|
||||||
@@ -638,6 +715,58 @@ actions:
|
|||||||
name: Disable Spotlight indexing
|
name: Disable Spotlight indexing
|
||||||
code: sudo mdutil -i off -d /
|
code: sudo mdutil -i off -d /
|
||||||
revertCode: sudo mdutil -i on /
|
revertCode: sudo mdutil -i on /
|
||||||
|
-
|
||||||
|
name: Disable Personalized advertisements and identifier collection
|
||||||
|
recommend: standard
|
||||||
|
docs: |-
|
||||||
|
This script enhances your privacy by deactivating Personalized Ads and disabling the collection
|
||||||
|
of identifiers related to your device. The process involves modifying certain key configurations,
|
||||||
|
which prevents Apple's advertising platform from using your personal information to deliver targeted
|
||||||
|
ads [1].
|
||||||
|
|
||||||
|
When Personalized Ads is enabled, your information may be used to provide ads that closely align
|
||||||
|
with your interests [1]. You might occasionally encounter such targeted ads in Apple News, Stocks,
|
||||||
|
and the Mac App Store [2]. Disabling Personalized Ads will prevent Apple from using your data for
|
||||||
|
ad targeting [2]. Although this does not necessarily decrease the quantity of ads you receive,
|
||||||
|
it may result in the ads being less relevant to your interests [2].
|
||||||
|
|
||||||
|
The primary keys to deactivating personalized ads are:
|
||||||
|
|
||||||
|
- **`allowApplePersonalizedAdvertising`**: If set to false, this restricts Apple's personalized
|
||||||
|
advertising [3]. This is applicable on macOS 12 and subsequent versions [3].
|
||||||
|
- **`allowIdentifierForAdvertising`**: The `advertisingIdentifier` is a unique string assigned
|
||||||
|
to each device [5]. Apple uses this identifier and recommends its use in third-party
|
||||||
|
applications for tasks like frequency capping, attribution, conversion events, estimating the
|
||||||
|
number of unique users, detecting advertising fraud, and debugging [5]. Although there is no
|
||||||
|
official documentation on it, a discussion on JAMF.com corroborates its existence [6].
|
||||||
|
|
||||||
|
My tests show that disabling any of the keys mentioned above results in the
|
||||||
|
"System Preferences > Apple Advertising > Personalized ads" option being deactivated in the GUI,
|
||||||
|
starting from macOS Monterey.
|
||||||
|
|
||||||
|
Please note: The `forceLimitAdTracking` key limits ad tracking [3] [4] and is found in CIS
|
||||||
|
benchmarks for macOS [4]. However, the official macOS documentation specifies that it is
|
||||||
|
applicable only to iOS 7 and later versions, not to macOS [3]. The key does not exist on the OS
|
||||||
|
by default.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20230731152633/https://www.apple.com/legal/privacy/data/en/apple-advertising/ "Legal - Apple Advertising & Privacy - Apple"
|
||||||
|
[2]: https://web.archive.org/web/20220805052411/https://support.apple.com/en-sg/guide/mac-help/mh32356/mac: "Change Privacy preferences on Mac - Apple Support (SG)"
|
||||||
|
[3]: https://web.archive.org/web/20230731155827/https://developer.apple.com/documentation/devicemanagement/restrictions "Restrictions | Apple Developer Documentation"
|
||||||
|
[4]: https://web.archive.org/web/20230731155653/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_11_0_Big_Sur_Benchmark_v2_0_0.pdf "CIS Apple macOS 11.0 Big Sur Benchmark"
|
||||||
|
[5]: https://web.archive.org/web/20230731155131/https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier "advertisingIdentifier | Apple Developer Documentation"
|
||||||
|
[6]: https://web.archive.org/web/20230731154840/https://community.jamf.com/t5/jamf-pro/macos-quot-limit-ad-tracking-quot/td-p/217001 'Solved: macOS "Limit Ad Tracking" - Jamf Nation Community - 217001'
|
||||||
|
code: |-
|
||||||
|
defaults write com.apple.AdLib allowIdentifierForAdvertising -bool false
|
||||||
|
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool false
|
||||||
|
defaults write com.apple.AdLib forceLimitAdTracking -bool true
|
||||||
|
# Default: (`defaults read com.apple.AdLib`)
|
||||||
|
# - `defaults read com.apple.AdLib allowApplePersonalizedAdvertising`: true (1)
|
||||||
|
# - `defaults read com.apple.AdLib allowIdentifierForAdvertising`: true (1)
|
||||||
|
# - `defaults read com.apple.AdLib forceLimitAdTracking`: non-existing
|
||||||
|
revertCode: |-
|
||||||
|
defaults write com.apple.AdLib allowIdentifierForAdvertising -bool true
|
||||||
|
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool true
|
||||||
|
sudo defaults delete com.apple.AdLib forceLimitAdTracking
|
||||||
-
|
-
|
||||||
category: Security improvements
|
category: Security improvements
|
||||||
children:
|
children:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly documentationUrls: ReadonlyArray<string>,
|
public readonly docs: ReadonlyArray<string>,
|
||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>,
|
public readonly scripts?: ReadonlyArray<IScript>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface IDocumentable {
|
export interface IDocumentable {
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly docs: ReadonlyArray<string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Version } from '@/domain/Version';
|
|||||||
export interface IProjectInformation {
|
export interface IProjectInformation {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly version: Version;
|
readonly version: Version;
|
||||||
|
|
||||||
|
readonly slogan: string;
|
||||||
readonly repositoryUrl: string;
|
readonly repositoryUrl: string;
|
||||||
readonly homepage: string;
|
readonly homepage: string;
|
||||||
readonly feedbackUrl: string;
|
readonly feedbackUrl: string;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { IScriptCode } from './IScriptCode';
|
|||||||
export interface IScript extends IEntity<string>, IDocumentable {
|
export interface IScript extends IEntity<string>, IDocumentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly level?: RecommendationLevel;
|
readonly level?: RecommendationLevel;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly docs: ReadonlyArray<string>;
|
||||||
readonly code: IScriptCode;
|
readonly code: IScriptCode;
|
||||||
canRevert(): boolean;
|
canRevert(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly version: Version,
|
public readonly version: Version,
|
||||||
|
public readonly slogan: string,
|
||||||
public readonly repositoryUrl: string,
|
public readonly repositoryUrl: string,
|
||||||
public readonly homepage: string,
|
public readonly homepage: string,
|
||||||
) {
|
) {
|
||||||
@@ -18,6 +19,9 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error('undefined version');
|
throw new Error('undefined version');
|
||||||
}
|
}
|
||||||
|
if (!slogan) {
|
||||||
|
throw new Error('undefined slogan');
|
||||||
|
}
|
||||||
if (!repositoryUrl) {
|
if (!repositoryUrl) {
|
||||||
throw new Error('repositoryUrl is undefined');
|
throw new Error('repositoryUrl is undefined');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly code: IScriptCode,
|
public readonly code: IScriptCode,
|
||||||
public readonly documentationUrls: ReadonlyArray<string>,
|
public readonly docs: ReadonlyArray<string>,
|
||||||
public readonly level?: RecommendationLevel,
|
public readonly level?: RecommendationLevel,
|
||||||
) {
|
) {
|
||||||
super(name);
|
super(name);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error(`missing code (script: ${name})`);
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
validateLevel(level);
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,18 @@ export class ScriptCode implements IScriptCode {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string,
|
||||||
syntax: ILanguageSyntax,
|
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
validateCode(execute);
|
||||||
validateCode(execute, syntax);
|
validateRevertCode(revert, execute);
|
||||||
validateRevertCode(revert, execute, syntax);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILanguageSyntax {
|
function validateRevertCode(revertCode: string, execute: string) {
|
||||||
readonly commentDelimiters: string[];
|
|
||||||
readonly commonCodeParts: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
|
|
||||||
if (!revertCode) {
|
if (!revertCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
validateCode(revertCode, syntax);
|
validateCode(revertCode);
|
||||||
if (execute === revertCode) {
|
if (execute === revertCode) {
|
||||||
throw new Error('Code itself and its reverting code cannot be the same');
|
throw new Error('Code itself and its reverting code cannot be the same');
|
||||||
}
|
}
|
||||||
@@ -31,54 +24,8 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
function validateCode(code: string): void {
|
||||||
if (!code || code.length === 0) {
|
if (!code || code.length === 0) {
|
||||||
throw new Error('missing code');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
ensureNoEmptyLines(code);
|
|
||||||
ensureCodeHasUniqueLines(code, syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoEmptyLines(code: string): void {
|
|
||||||
const lines = code.split(/\r\n|\r|\n/);
|
|
||||||
if (lines.some((line) => line.trim().length === 0)) {
|
|
||||||
throw Error(`Script has empty lines:\n${lines.map((part, index) => `\n (${index}) ${part || '❌'}`).join('')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
|
||||||
const allLines = code.split(/\r\n|\r|\n/);
|
|
||||||
const checkedLines = allLines.filter((line) => !shouldIgnoreLine(line, syntax));
|
|
||||||
if (checkedLines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicateLines = checkedLines.filter((e, i, a) => a.indexOf(e) !== i);
|
|
||||||
if (duplicateLines.length !== 0) {
|
|
||||||
throw Error(`Duplicates detected in script:\n${printDuplicatedLines(allLines)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printDuplicatedLines(allLines: string[]) {
|
|
||||||
return allLines
|
|
||||||
.map((line, index) => {
|
|
||||||
const occurrenceIndices = allLines
|
|
||||||
.map((e, i) => (e === line ? i : ''))
|
|
||||||
.filter(String);
|
|
||||||
const isDuplicate = occurrenceIndices.length > 1;
|
|
||||||
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
|
|
||||||
return `${indicator}[${index}] ${line}`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
|
||||||
const lowerCaseCodeLine = codeLine.toLowerCase();
|
|
||||||
const isCommentLine = () => syntax.commentDelimiters.some(
|
|
||||||
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
|
||||||
);
|
|
||||||
const consistsOfFrequentCommands = () => {
|
|
||||||
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
|
||||||
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
|
||||||
};
|
|
||||||
return isCommentLine() || consistsOfFrequentCommands();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export class CodeRunner {
|
|||||||
|
|
||||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||||
switch (environment.os) {
|
switch (environment.os) {
|
||||||
|
case OperatingSystem.Linux:
|
||||||
|
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||||
case OperatingSystem.macOS:
|
case OperatingSystem.macOS:
|
||||||
return `open -a Terminal.app ${scriptPath}`;
|
return `open -a Terminal.app ${scriptPath}`;
|
||||||
// Another option with graphical sudo would be
|
// Another option with graphical sudo would be
|
||||||
|
|||||||
6
src/presentation/assets/icons/external-link.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v9l-3.794-3.793-5.999 6-1.414-1.414 5.999-6L12 3h9z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 302 B |
@@ -4,18 +4,19 @@
|
|||||||
|
|
||||||
@use "@/presentation/assets/styles/colors" as *;
|
@use "@/presentation/assets/styles/colors" as *;
|
||||||
@use "@/presentation/assets/styles/fonts" as *;
|
@use "@/presentation/assets/styles/fonts" as *;
|
||||||
|
@use "@/presentation/assets/styles/mixins" as *;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$globals-color-hover: $color-primary;
|
||||||
a {
|
a {
|
||||||
color:inherit;
|
color:inherit;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
@include hover-or-touch {
|
||||||
color: $color-primary;
|
color: $globals-color-hover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/presentation/assets/styles/_mixins.scss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@mixin hover-or-touch($selector-suffix: '', $selector-prefix: '&') {
|
||||||
|
@media (hover: hover) {
|
||||||
|
/* We only do this if hover is truly supported; otherwise the emulator in mobile
|
||||||
|
keeps hovered style in-place even after touching, making it sticky. */
|
||||||
|
#{$selector-prefix}:hover #{$selector-suffix} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (hover: none) {
|
||||||
|
/* We only do this if hover is not supported,otherwise the desktop behavior is not
|
||||||
|
as desired; it does not get activated on hover but only during click/touch. */
|
||||||
|
#{$selector-prefix}:active #{$selector-suffix} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin clickable($cursor: 'pointer') {
|
||||||
|
cursor: #{$cursor};
|
||||||
|
user-select: none;
|
||||||
|
/*
|
||||||
|
It removes (blue) background during touch as seen in mobile webkit browsers (Chrome, Safari, Edge).
|
||||||
|
The default behavior is that any element (or containing element) that has cursor:pointer
|
||||||
|
explicitly set and is clicked will flash blue momentarily.
|
||||||
|
Removing it could have accessibility issue since that hides an interactive cue. But as we still provide
|
||||||
|
response to user actions through :active by `hover-or-touch` mixin.
|
||||||
|
*/
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
@forward "./media";
|
@forward "./media";
|
||||||
@forward "./colors";
|
@forward "./colors";
|
||||||
@forward "./globals";
|
@forward "./globals";
|
||||||
|
@forward "./mixins";
|
||||||
|
|
||||||
@forward "./components/card";
|
@forward "./components/card";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Overrides base styling for LiquorTree
|
// Overrides base styling for LiquorTree
|
||||||
@use "@/presentation/assets/styles/colors" as *;
|
@use "@/presentation/assets/styles/colors" as *;
|
||||||
|
@use "@/presentation/assets/styles/mixins" as *;
|
||||||
|
|
||||||
$color-tree-bg : $color-primary-darker;
|
$color-tree-bg : $color-primary-darker;
|
||||||
$color-node-arrow : $color-on-primary;
|
$color-node-arrow : $color-on-primary;
|
||||||
@@ -18,14 +19,17 @@ $color-node-checkbox-tick-checked : $color-on-secondary;
|
|||||||
&-node {
|
&-node {
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
> .tree-content {
|
> .tree-content {
|
||||||
> .tree-anchor > span {
|
> .tree-anchor {
|
||||||
color: $color-node-fg;
|
> span {
|
||||||
text-transform: uppercase;
|
color: $color-node-fg;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
display: block; // so it takes full width to allow aligning items inside
|
||||||
}
|
}
|
||||||
&:hover {
|
@include hover-or-touch {
|
||||||
background: $color-node-hover-bg !important;
|
background: $color-node-hover-bg !important;
|
||||||
}
|
}
|
||||||
|
background: $color-tree-bg !important; // If not styled, it gets white background on mobile.
|
||||||
}
|
}
|
||||||
&.selected { // When using keyboard navigation it highlights current item and its child items
|
&.selected { // When using keyboard navigation it highlights current item and its child items
|
||||||
background: $color-node-keyboard-bg;
|
background: $color-node-keyboard-bg;
|
||||||
|
|||||||
@@ -42,17 +42,18 @@ export default class IconButton extends Vue {
|
|||||||
box-shadow: 0 3px 9px $color-primary-darkest;
|
box-shadow: 0 3px 9px $color-primary-darkest;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
cursor: pointer;
|
@include clickable;
|
||||||
|
|
||||||
width: 10%;
|
width: 10%;
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
&:hover {
|
@include hover-or-touch {
|
||||||
background: $color-surface;
|
background: $color-surface;
|
||||||
box-shadow: 0px 2px 10px 5px $color-secondary;
|
box-shadow: 0px 2px 10px 5px $color-secondary;
|
||||||
}
|
}
|
||||||
&:hover>&__text {
|
@include hover-or-touch('>&__text') {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
&:hover>&__icon {
|
@include hover-or-touch('>&__icon') {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
&__text {
|
&__text {
|
||||||
@@ -62,6 +63,9 @@ export default class IconButton extends Vue {
|
|||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
@include hover-or-touch {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -28,13 +28,14 @@ export default class Code extends Vue {
|
|||||||
@use "@/presentation/assets/styles/main" as *;
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
.code-wrapper {
|
.code-wrapper {
|
||||||
|
display:flex;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-family: $font-normal;
|
font-family: $font-normal;
|
||||||
background-color: $color-primary-darker;
|
background-color: $color-primary-darker;
|
||||||
color: $color-on-primary;
|
color: $color-on-primary;
|
||||||
padding-left: 0.3rem;
|
align-items: center;
|
||||||
padding-right: 0.3rem;
|
padding: 0.2rem;
|
||||||
.dollar {
|
.dollar {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -42,13 +43,13 @@ export default class Code extends Vue {
|
|||||||
}
|
}
|
||||||
.copy-button {
|
.copy-button {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
cursor: pointer;
|
@include clickable;
|
||||||
&:hover {
|
@include hover-or-touch {
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
font-size: 1.2rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { IInstructionListData, IInstructionListStep } from '../InstructionListData';
|
||||||
|
|
||||||
|
export interface IInstructionsBuilderData {
|
||||||
|
readonly fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstructionStepBuilderType = (data: IInstructionsBuilderData) => IInstructionListStep;
|
||||||
|
|
||||||
|
export class InstructionsBuilder {
|
||||||
|
private readonly stepBuilders = new Array<InstructionStepBuilderType>();
|
||||||
|
|
||||||
|
constructor(private readonly os: OperatingSystem) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public withStep(stepBuilder: InstructionStepBuilderType) {
|
||||||
|
if (!stepBuilder) { throw new Error('missing stepBuilder'); }
|
||||||
|
this.stepBuilders.push(stepBuilder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(data: IInstructionsBuilderData): IInstructionListData {
|
||||||
|
if (!data) { throw new Error('missing data'); }
|
||||||
|
return {
|
||||||
|
operatingSystem: this.os,
|
||||||
|
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { InstructionsBuilder } from './InstructionsBuilder';
|
||||||
|
|
||||||
|
export class LinuxInstructionsBuilder extends InstructionsBuilder {
|
||||||
|
constructor() {
|
||||||
|
super(OperatingSystem.Linux);
|
||||||
|
super
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Download the file.',
|
||||||
|
details: 'You should have already been prompted to save the script file.'
|
||||||
|
+ '<br/>If this was not the case or you did not save the script when prompted,'
|
||||||
|
+ '<br/>please try to download your script file again.',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Open terminal.',
|
||||||
|
details:
|
||||||
|
'Opening terminal changes based on the distro you run.'
|
||||||
|
+ '<br/>You may search for "Terminal" in your application launcher to find it.'
|
||||||
|
+ '<br/>'
|
||||||
|
+ '<br/>Alternatively use terminal shortcut for your distro if it has one by default:'
|
||||||
|
+ '<ul>'
|
||||||
|
+ '<li><code>Ctrl-Alt-T</code>: Ubuntu, CentOS, Linux Mint, Elementary OS, ubermix, Kali…</li>'
|
||||||
|
+ '<li><code>Super-T</code>: Pop!_OS…</li>'
|
||||||
|
+ '<li><code>Alt-T</code>: Parrot OS…</li>'
|
||||||
|
+ '<li><code>Ctrl-Alt-Insert</code>: Bodhi Linux…</li>'
|
||||||
|
+ '</ul>'
|
||||||
|
+ '<br/>'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: 'cd ~/Downloads',
|
||||||
|
details: 'Press on <code>enter/return</code> key after running the command.'
|
||||||
|
+ '<br/>If the file is not downloaded on Downloads folder,'
|
||||||
|
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
|
||||||
|
+ '<br/>'
|
||||||
|
+ '<br/>This command means:'
|
||||||
|
+ '<ul>'
|
||||||
|
+ '<li><code>cd</code> will change the current folder.</li>'
|
||||||
|
+ '<li><code>~</code> is the user home directory.</li>'
|
||||||
|
+ '</ul>',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep((data) => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Give the file execute permissions:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: `chmod +x ${data.fileName}`,
|
||||||
|
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
|
||||||
|
+ 'It will make the file executable. <br/>'
|
||||||
|
+ 'If you use desktop environment you can alternatively (instead of running the command):'
|
||||||
|
+ '<ol>'
|
||||||
|
+ '<li>Locate the file using your file manager.</li>'
|
||||||
|
+ '<li>Right click on the file, select "Properties".</li>'
|
||||||
|
+ '<li>Go to "Permissions" and check "Allow executing file as program".</li>'
|
||||||
|
+ '</ol>'
|
||||||
|
+ '<br/>These GUI steps and name of options may change depending on your file manager.'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep((data) => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Execute the file:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: `./${data.fileName}`,
|
||||||
|
details:
|
||||||
|
'If you have desktop environment, instead of running this command you can alternatively:'
|
||||||
|
+ '<ol>'
|
||||||
|
+ '<li>Locate the file using your file manager.</li>'
|
||||||
|
+ '<li>Right click on the file, select "Run as program".</li>'
|
||||||
|
+ '</ol>'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'If asked, enter your administrator password.',
|
||||||
|
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
|
||||||
|
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
|
||||||
|
+ '<br/>Administrator privileges are required to configure OS.',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { InstructionsBuilder } from './InstructionsBuilder';
|
||||||
|
|
||||||
|
export class MacOsInstructionsBuilder extends InstructionsBuilder {
|
||||||
|
constructor() {
|
||||||
|
super(OperatingSystem.macOS);
|
||||||
|
super
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Download the file.',
|
||||||
|
details: 'You should have already been prompted to save the script file.'
|
||||||
|
+ '<br/>If this was not the case or you did not save the script when prompted,'
|
||||||
|
+ '<br/>please try to download your script file again.'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Open terminal.',
|
||||||
|
details: 'Type Terminal into Spotlight or open it from the Applications -> Utilities folder.',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: 'cd ~/Downloads',
|
||||||
|
details: 'Press on <code>enter/return</code> key after running the command.'
|
||||||
|
+ '<br/>If the file is not downloaded on Downloads folder,'
|
||||||
|
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
|
||||||
|
+ '<br/>'
|
||||||
|
+ '<br/>This command means:'
|
||||||
|
+ '<ul>'
|
||||||
|
+ '<li><code>cd</code> will change the current folder.</li>'
|
||||||
|
+ '<li><code>~</code> is the user home directory.</li>'
|
||||||
|
+ '</ul>',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep((data) => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Give the file execute permissions:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: `chmod +x ${data.fileName}`,
|
||||||
|
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
|
||||||
|
+ 'It will make the file executable.'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep((data) => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Execute the file:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: `./${data.fileName}`,
|
||||||
|
details: 'Alternatively you can locate the file in <strong>Finder</strong> and double click on it.',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'If asked, enter your administrator password.',
|
||||||
|
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
|
||||||
|
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
|
||||||
|
+ '<br/>Administrator privileges are required to configure OS.'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="instructions">
|
||||||
|
<p>
|
||||||
|
You have two alternatives to apply your selection.
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<strong>1. The easy alternative</strong>. Run your script without any manual steps by
|
||||||
|
<a :href="this.macOsDownloadUrl">downloading desktop version</a> of {{ this.appName }} on the
|
||||||
|
{{ this.osName }} system you wish to configure, and then click on the Run button. This is
|
||||||
|
recommended for most users.
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<strong>2. The hard (manual) alternative</strong>. This requires you to do additional manual
|
||||||
|
steps. If you are unsure how to follow the instructions, hover on information
|
||||||
|
(<font-awesome-icon :icon="['fas', 'info-circle']" />)
|
||||||
|
icons near the steps, or follow the easy alternative described above.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<ol>
|
||||||
|
<li
|
||||||
|
v-for='(step, index) in this.data.steps'
|
||||||
|
v-bind:key="index"
|
||||||
|
class="step"
|
||||||
|
>
|
||||||
|
<div class="step__action">
|
||||||
|
<span>{{ step.action.instruction }}</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="step.action.details"
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="step.action.details"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="step.code" class="step__code">
|
||||||
|
<Code>{{ step.code.instruction }}</Code>
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="step.code.details"
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="step.code.details"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||||
|
import Code from './Code.vue';
|
||||||
|
import { IInstructionListData } from './InstructionListData';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
Code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class InstructionList extends Vue {
|
||||||
|
public appName = '';
|
||||||
|
|
||||||
|
public macOsDownloadUrl = '';
|
||||||
|
|
||||||
|
public osName = '';
|
||||||
|
|
||||||
|
@Prop() public data: IInstructionListData;
|
||||||
|
|
||||||
|
public async created() {
|
||||||
|
if (!this.data) {
|
||||||
|
throw new Error('missing data');
|
||||||
|
}
|
||||||
|
const app = await ApplicationFactory.Current.getApp();
|
||||||
|
this.appName = app.info.name;
|
||||||
|
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
|
||||||
|
this.osName = renderOsName(this.data.operatingSystem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOsName(os: OperatingSystem): string {
|
||||||
|
switch (os) {
|
||||||
|
case OperatingSystem.Windows: return 'Windows';
|
||||||
|
case OperatingSystem.macOS: return 'macOS';
|
||||||
|
case OperatingSystem.Linux: return 'Linux';
|
||||||
|
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
|
.step {
|
||||||
|
margin: 10px 0;
|
||||||
|
&__action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&__code {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.explanation {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export interface IInstructionListData {
|
||||||
|
readonly operatingSystem: OperatingSystem;
|
||||||
|
readonly steps: readonly IInstructionListStep[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInstructionListStep {
|
||||||
|
readonly action: IInstructionInfo;
|
||||||
|
readonly code?: IInstructionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInstructionInfo {
|
||||||
|
readonly instruction: string;
|
||||||
|
readonly details?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { InstructionsBuilder } from './Data/InstructionsBuilder';
|
||||||
|
import { MacOsInstructionsBuilder } from './Data/MacOsInstructionsBuilder';
|
||||||
|
import { IInstructionListData } from './InstructionListData';
|
||||||
|
import { LinuxInstructionsBuilder } from './Data/LinuxInstructionsBuilder';
|
||||||
|
|
||||||
|
const builders = new Map<OperatingSystem, InstructionsBuilder>([
|
||||||
|
[OperatingSystem.macOS, new MacOsInstructionsBuilder()],
|
||||||
|
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function hasInstructions(os: OperatingSystem) {
|
||||||
|
return builders.has(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInstructions(
|
||||||
|
os: OperatingSystem,
|
||||||
|
fileName: string,
|
||||||
|
): IInstructionListData {
|
||||||
|
return builders
|
||||||
|
.get(os)
|
||||||
|
.build({ fileName });
|
||||||
|
}
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="instructions">
|
|
||||||
<p>
|
|
||||||
Since you're using online version of {{ this.appName }}, you will need to do additional
|
|
||||||
steps after downloading the file to execute your script on macOS:
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<span>Download the file</span>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="explanation"
|
|
||||||
:icon="['fas', 'info-circle']"
|
|
||||||
v-tooltip.top-center="
|
|
||||||
'You should be prompted to save the script file now'
|
|
||||||
+ ', otherwise try to download it again'"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span>Open terminal</span>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="explanation"
|
|
||||||
:icon="['fas', 'info-circle']"
|
|
||||||
v-tooltip.top-center="
|
|
||||||
'Type Terminal into Spotlight or open from the Applications -> Utilities folder'"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span>Navigate to the folder where you downloaded the file e.g.:</span>
|
|
||||||
<div>
|
|
||||||
<Code>cd ~/Downloads</Code>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="explanation"
|
|
||||||
:icon="['fas', 'info-circle']"
|
|
||||||
v-tooltip.top-center="
|
|
||||||
'Press on Enter/Return key after running the command.<br/>'
|
|
||||||
+ 'If the file is not downloaded on Downloads folder, change'
|
|
||||||
+ '`Downloads` to path where the file is downloaded.<br/>'
|
|
||||||
+ '• `cd` will change the current folder.<br/>'
|
|
||||||
+ '• `~` is the user home directory.'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span>Give the file execute permissions:</span>
|
|
||||||
<div>
|
|
||||||
<Code>chmod +x {{ this.fileName }}</Code>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="explanation"
|
|
||||||
:icon="['fas', 'info-circle']"
|
|
||||||
v-tooltip.top-center="
|
|
||||||
'Press on Enter/Return key after running the command.<br/>' +
|
|
||||||
'It will make the file executable.'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span>Execute the file:</span>
|
|
||||||
<div>
|
|
||||||
<Code>./{{ this.fileName }}</Code>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="explanation"
|
|
||||||
:icon="['fas', 'info-circle']"
|
|
||||||
v-tooltip.top-center="'Alternatively you can double click on the file'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span>If asked, enter your administrator password</span>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="explanation"
|
|
||||||
:icon="['fas', 'info-circle']"
|
|
||||||
v-tooltip.top-center="
|
|
||||||
'Press on Enter/Return key after typing your password<br/>' +
|
|
||||||
'Your password will not be shown by default.<br/>' +
|
|
||||||
'Administor privileges are required to configure OS.'"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts
|
|
||||||
directly to skip these steps.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
|
||||||
import Code from './Code.vue';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
Code,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class MacOsInstructions extends Vue {
|
|
||||||
@Prop() public fileName: string;
|
|
||||||
|
|
||||||
public appName = '';
|
|
||||||
|
|
||||||
public macOsDownloadUrl = '';
|
|
||||||
|
|
||||||
public async created() {
|
|
||||||
const app = await ApplicationFactory.Current.getApp();
|
|
||||||
this.appName = app.info.name;
|
|
||||||
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@use "@/presentation/assets/styles/main" as *;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
.explanation {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
v-on:click="copyCode"
|
v-on:click="copyCode"
|
||||||
icon-prefix="fas" icon-name="copy">
|
icon-prefix="fas" icon-name="copy">
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Dialog v-if="this.isMacOsCollection" ref="instructionsDialog">
|
<Dialog v-if="this.hasInstructions" ref="instructionsDialog">
|
||||||
<MacOsInstructions :fileName="this.fileName" />
|
<InstructionList :data="this.instructions" />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -35,15 +35,18 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
||||||
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import MacOsInstructions from './MacOsInstructions.vue';
|
import InstructionList from './Instructions/InstructionList.vue';
|
||||||
import IconButton from './IconButton.vue';
|
import IconButton from './IconButton.vue';
|
||||||
|
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||||
|
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
IconButton,
|
IconButton,
|
||||||
MacOsInstructions,
|
InstructionList,
|
||||||
Dialog,
|
Dialog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -54,7 +57,9 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
|
|
||||||
public hasCode = false;
|
public hasCode = false;
|
||||||
|
|
||||||
public isMacOsCollection = false;
|
public instructions: IInstructionListData | undefined;
|
||||||
|
|
||||||
|
public hasInstructions = false;
|
||||||
|
|
||||||
public fileName = '';
|
public fileName = '';
|
||||||
|
|
||||||
@@ -66,7 +71,7 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
public async saveCode() {
|
public async saveCode() {
|
||||||
const context = await this.getCurrentContext();
|
const context = await this.getCurrentContext();
|
||||||
saveCode(this.fileName, context.state);
|
saveCode(this.fileName, context.state);
|
||||||
if (this.isMacOsCollection) {
|
if (this.hasInstructions) {
|
||||||
(this.$refs.instructionsDialog as Dialog).show();
|
(this.$refs.instructionsDialog as Dialog).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,11 +82,9 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
|
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
|
||||||
const isNewOs = (test: OperatingSystem) => newState.collection.os === test;
|
this.updateRunState(newState.os);
|
||||||
this.canRun = this.isDesktopVersion && isNewOs(Environment.CurrentEnvironment.os);
|
this.updateDownloadState(newState.collection);
|
||||||
this.isMacOsCollection = isNewOs(OperatingSystem.macOS);
|
this.updateCodeState(newState.code);
|
||||||
this.fileName = buildFileName(newState.collection.scripting);
|
|
||||||
this.react(newState.code);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCurrentCode(): Promise<IApplicationCode> {
|
private async getCurrentCode(): Promise<IApplicationCode> {
|
||||||
@@ -90,7 +93,20 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async react(code: IApplicationCode) {
|
private updateRunState(selectedOs: OperatingSystem) {
|
||||||
|
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
|
||||||
|
this.canRun = this.isDesktopVersion && isRunningOnSelectedOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateDownloadState(collection: ICategoryCollection) {
|
||||||
|
this.fileName = buildFileName(collection.scripting);
|
||||||
|
this.hasInstructions = hasInstructions(collection.os);
|
||||||
|
if (this.hasInstructions) {
|
||||||
|
this.instructions = getInstructions(collection.os, this.fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCodeState(code: IApplicationCode) {
|
||||||
this.hasCode = code.current && code.current.length > 0;
|
this.hasCode = code.current && code.current.length > 0;
|
||||||
this.events.unsubscribeAll();
|
this.events.unsubscribeAll();
|
||||||
this.events.register(code.changed.on((newCode) => {
|
this.events.register(code.changed.on((newCode) => {
|
||||||
@@ -131,7 +147,6 @@ async function executeCode(context: IReadOnlyApplicationContext) {
|
|||||||
/* fileExtension: */ context.state.collection.scripting.fileExtension,
|
/* fileExtension: */ context.state.collection.scripting.fileExtension,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ function getLanguage(language: ScriptingLanguage) {
|
|||||||
function getDefaultCode(language: ScriptingLanguage): string {
|
function getDefaultCode(language: ScriptingLanguage): string {
|
||||||
return new CodeBuilderFactory()
|
return new CodeBuilderFactory()
|
||||||
.create(language)
|
.create(language)
|
||||||
.appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows and macOS')
|
.appendCommentLine('privacy.sexy — Now you have the choice.')
|
||||||
|
.appendCommentLine(' 🔐 Enforce privacy & security best-practices on Windows, macOS and Linux.')
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendCommentLine('-- 🤔 How to use')
|
.appendCommentLine('-- 🤔 How to use')
|
||||||
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
|
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
|
||||||
|
|||||||