Compare commits

..

2 Commits

Author SHA1 Message Date
undergroundwires
cfe5704328 Add more macOS scripts
TODO: https://github.com/usnistgov/macos_security/tree/main/rules
2022-10-12 17:08:53 +02:00
undergroundwires
d16846fa3c Add donation information 2022-03-03 00:27:48 +01:00
200 changed files with 21126 additions and 23950 deletions

View File

@@ -1,3 +1,2 @@
> 1% > 1%
last 2 versions last 2 versions
not dead

View File

@@ -1 +0,0 @@
dist/

View File

@@ -27,14 +27,7 @@ module.exports = {
'@vue/typescript/recommended', '@vue/typescript/recommended',
], ],
parserOptions: { parserOptions: {
ecmaVersion: 12, // ECMA 2021 ecmaVersion: 'latest',
/*
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(),

View File

@@ -21,9 +21,8 @@ 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: Open "Start button" > "Settings" > "System" > "About". On Windows you can find it using "Start button" > "Settings" > "System" > "About".
On macOS: Open "Apple menu (top left corner)" > "About This Mac". On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
--> -->
### Reproduction steps ### Reproduction steps

View File

@@ -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?
One of the supported OSes: "Windows", "macOS" or "Linux". Either "Windows" or "macOS".
--> -->
### Name ### Name
@@ -31,11 +31,9 @@ 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

View File

@@ -1,8 +0,0 @@
runs:
using: composite
steps:
-
name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16.x

View File

@@ -18,7 +18,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci
@@ -40,7 +42,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci
@@ -53,23 +57,3 @@ 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

View File

@@ -19,7 +19,9 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup node - name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Lint - name: Lint

View File

@@ -16,7 +16,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: NPM audit name: NPM audit
run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit

View File

@@ -20,7 +20,9 @@ jobs:
- name: Checkout to bump commit - name: Checkout to bump commit
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)" run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
- name: Setup node - name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run unit tests - name: Run unit tests

View File

@@ -1,8 +1,8 @@
name: release-site name: release-site
on: on:
release: release:
types: [created] # will be triggered when a NON-draft release is created and published. types: [created] # will be triggered when a NON-draft release is created and published.
jobs: jobs:
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
@@ -77,28 +77,30 @@ jobs:
name: "App: Checkout" name: "App: Checkout"
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
path: app path: site
ref: master # otherwise we don't get version bump commit ref: master # otherwise we don't get version bump commit
- -
name: "App: Setup node" name: "App: Setup node"
uses: ./app/.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: "App: Install dependencies" name: "App: Install dependencies"
run: npm ci run: npm ci
working-directory: app working-directory: site
- -
name: "App: Run unit tests" name: "App: Run unit tests"
run: npm run test:unit run: npm run test:unit
working-directory: app working-directory: site
- -
name: "App: Build" name: "App: Build"
run: npm run build run: npm run build
working-directory: app working-directory: site
- -
name: "App: Deploy to S3" name: "App: Deploy to S3"
run: >- run: >-
bash "aws/scripts/deploy/deploy-to-s3.sh" \ bash "aws/scripts/deploy/deploy-to-s3.sh" \
--folder app/dist \ --folder site/dist \
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \ --web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
--storage-class ONEZONE_IA \ --storage-class ONEZONE_IA \
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \ --role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \

View File

@@ -17,7 +17,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci

View File

@@ -19,7 +19,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci

View File

@@ -16,8 +16,10 @@ jobs:
name: Checkout name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- -
name: Set-up node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci

View File

@@ -1,30 +1,5 @@
# 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)

View File

@@ -1,6 +1,6 @@
# privacy.sexy — Now you have the choice # privacy.sexy
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆 > Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
<!-- markdownlint-disable MD033 --> <!-- markdownlint-disable MD033 -->
<p align="center"> <p align="center">
@@ -120,15 +120,15 @@ 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.
## Support ## Support
**Sponsor 💕**. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate). **Sponsor 💕**. This project is free, and it might not be tempting to donate since you don't have to pay. But your donations will ensure that this project stays alive. A monthly coffee from you would make a difference. Recurring donations allow me to spend more time and resources on this project. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate).
**Star 🤩**. Feel free to give it a star ⭐ . **Star 🤩**. I know that not everyone can afford donating a coffee to show support. In this case, feel free to give it a star ⭐ . It helps me to see that you appreciate the project.
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts). **Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).

View File

@@ -1,5 +1,5 @@
# build # build
This folder contains files that are used by Electron to serve the desktop version. - These are the file that are used by electron.
- Logos are created by from the [PNG icon](./../public/icon.png)
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md). - by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

View File

@@ -1,14 +0,0 @@
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',
},
});

3
cypress.json Normal file
View File

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

View File

@@ -12,9 +12,9 @@ Everything that's merged in the master goes directly to production.
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code. privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code.
GitHub workflows i.e. pipelines exist in [`/.github/workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] . GitHub workflows i.e. pipelines exist in [`/.github/.workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
Local GitHub actions are defined in [`/.github/actions/`](./../.github/actions/) and used to reuse same workflow steps. [1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
## Pipeline types ## Pipeline types

View File

@@ -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,12 +38,9 @@
- `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`
@@ -70,12 +67,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`*`, ... ]`
- Documentation pieces related to the script. - Single documentation URL or list of URLs for those who wants to learn more about the script
- Rendered as markdown. - E.g. `https://docs.microsoft.com/en-us/windows-server/`
- `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
@@ -123,7 +120,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)
@@ -136,7 +133,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
@@ -144,7 +141,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

View File

@@ -45,7 +45,6 @@ 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

View File

@@ -26,10 +26,6 @@ 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.

View File

@@ -10,19 +10,10 @@
- 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) but not the same. - Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language.
- 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
@@ -65,31 +56,9 @@ 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. 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 }}`.
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
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: Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
```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 }}`.

View File

@@ -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.config.ts`](./../cypress.config.ts): Cypress configuration file. - [`cypress.json`](./../cypress.json): 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.

View File

@@ -1,95 +0,0 @@
#!/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"

View File

@@ -1,12 +0,0 @@
# 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.

View File

@@ -1,127 +0,0 @@
#!/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();

View File

@@ -1,56 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

33735
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,8 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.11.4", "version": "0.11.3",
"private": true, "private": true,
"slogan": "Now you have the choice", "description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
"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",
@@ -11,96 +10,83 @@
"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": "index.js", "main": "background.js",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^6.4.0", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^6.4.0", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.9", "@fortawesome/vue-fontawesome": "^2.0.6",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.3.1",
"ace-builds": "^1.23.4", "ace-builds": "^1.4.13",
"core-js": "^3.32.0", "core-js": "^3.18.3",
"cross-fetch": "^4.0.0", "cross-fetch": "^3.1.4",
"electron-progressbar": "^2.1.0", "electron-progressbar": "^2.0.1",
"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",
"markdown-it": "^13.0.1", "npm": "^8.1.1",
"npm": "^9.8.1",
"v-tooltip": "2.1.3", "v-tooltip": "2.1.3",
"vue": "^2.7.14", "vue": "^2.6.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.47",
"@types/chai": "^4.3.5", "@types/chai": "^4.2.22",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.3",
"@types/mocha": "^10.0.1", "@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.0-rc.1",
"@vue/cli-plugin-e2e-cypress": "~5.0.8", "@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
"@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.0-rc.1",
"@vue/cli-plugin-typescript": "~5.0.8", "@vue/cli-plugin-typescript": "~5.0.0-rc.1",
"@vue/cli-plugin-unit-mocha": "~5.0.8", "@vue/cli-plugin-unit-mocha": "~5.0.0-rc.1",
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.0-rc.1",
"@vue/eslint-config-airbnb": "^6.0.0", "@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^9.1.0",
"chai": "^4.3.7", "@vue/test-utils": "1.2.2",
"cypress": "^12.17.2", "chai": "^4.3.4",
"electron": "^25.3.2", "cypress": "^8.3.0",
"electron-builder": "^24.6.3", "electron": "^15.3.0",
"electron-builder": "^22.14.13",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1", "electron-log": "^4.4.1",
"electron-log": "^4.4.8", "electron-updater": "^4.3.9",
"electron-updater": "^6.1.4", "eslint": "^7.32.0",
"eslint": "^8.46.0", "eslint-plugin-import": "^2.25.3",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-vue": "^8.0.3",
"eslint-plugin-vue": "^9.6.0", "eslint-plugin-vuejs-accessibility": "^1.1.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.35.0", "markdownlint-cli": "^0.29.0",
"remark-cli": "^11.0.0", "remark-cli": "^10.0.0",
"remark-lint-no-dead-urls": "^1.1.0", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^5.1.2", "remark-preset-lint-consistent": "^5.1.0",
"remark-validate-links": "^12.1.1", "remark-validate-links": "^11.0.1",
"sass": "^1.64.1", "sass": "^1.43.3",
"sass-loader": "^13.3.2", "sass-loader": "10.2.0",
"svgexport": "^0.4.2", "ts-loader": "9.0.1",
"ts-loader": "^9.4.4", "tslib": "^2.3.1",
"typescript": "~4.6.2", "typescript": "^4.4.4",
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4", "vue-cli-plugin-electron-builder": "^2.1.1",
"yaml-lint": "^1.7.0" "vue-template-compiler": "^2.6.14",
}, "yaml-lint": "^1.2.4"
"overrides": {
"vue-cli-plugin-electron-builder": {
"electron-builder": "^24.6.3"
}
}, },
"//devDependencies": { "//devDependencies": {
"typescript": [ "ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4"
"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": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -2,8 +2,9 @@
<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, macOS and Linux</title> <title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</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">

View File

@@ -1,5 +1,6 @@
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 {
@@ -58,12 +59,10 @@ export abstract class CodeBuilder implements ICodeBuilder {
} }
public toString(): string { public toString(): string {
return this.lines.join(this.getNewLineTerminator()); return this.lines.join(NewLine);
} }
protected abstract getCommentDelimiter(): string; protected abstract getCommentDelimiter(): string;
protected abstract writeStandardOut(text: string): string; protected abstract writeStandardOut(text: string): string;
protected abstract getNewLineTerminator(): string;
} }

View File

@@ -8,10 +8,6 @@ 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) {

View File

@@ -8,10 +8,6 @@ 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) {

View File

@@ -4,7 +4,6 @@ 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';
@@ -29,7 +28,7 @@ const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
}; };
const PreParsedCollections: readonly CollectionData [] = [ const PreParsedCollections: readonly CollectionData [] = [
WindowsData, MacOsData, LinuxData, WindowsData, MacOsData,
]; ];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(collections: readonly CollectionData[]) {

View File

@@ -3,9 +3,7 @@ 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 { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator'; import { parseDocUrls } from './DocumentationParser';
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';
@@ -14,67 +12,35 @@ 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'); }
return parseCategoryRecursively({ ensureValid(category);
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 context.categoryData.children) { for (const data of category.children) {
parseNode({ parseCategoryChild(data, children, category, context);
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 ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) { function ensureValid(category: CategoryData) {
new NodeValidator({ if (!category) {
type: NodeType.Category, throw Error('missing category');
selfNode: category, }
parentNode: parentCategory, if (!category.children || category.children.length === 0) {
}) throw Error(`category has no children: "${category.category}"`);
.assertDefined(category) }
.assertValidName(category.category) if (!category.category || category.category.length === 0) {
.assert( throw Error('category has no name');
() => category.children && category.children.length > 0, }
`"${category.category}" has no children.`,
);
} }
interface ICategoryChildren { interface ICategoryChildren {
@@ -82,29 +48,22 @@ interface ICategoryChildren {
subScripts: Script[]; subScripts: Script[];
} }
interface INodeParseContext { function parseCategoryChild(
readonly nodeData: CategoryOrScriptData; data: CategoryOrScriptData,
readonly children: ICategoryChildren; children: ICategoryChildren,
readonly parent: CategoryData; parent: CategoryData,
readonly factory: CategoryFactoryType; context: ICategoryCollectionParseContext,
readonly context: ICategoryCollectionParseContext; ) {
} if (isCategory(data)) {
function parseNode(context: INodeParseContext) { const subCategory = parseCategory(data as CategoryData, context);
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent }); children.subCategories.push(subCategory);
validator.assertDefined(context.nodeData); } else if (isScript(data)) {
if (isCategory(context.nodeData)) { const scriptData = data as ScriptData;
const subCategory = parseCategoryRecursively({ const script = parseScript(scriptData, context);
categoryData: context.nodeData as CategoryData, children.subScripts.push(script);
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 {
validator.throw('Node is neither a category or a script.'); throw new Error(`Child element is neither a category or a script.
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
} }
} }
@@ -114,22 +73,14 @@ function isScript(data: CategoryOrScriptData): data is ScriptData {
} }
function isCategory(data: CategoryOrScriptData): data is CategoryData { function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category'); const { category } = data as CategoryData;
return category && category.length > 0;
} }
function hasCode(data: InstructionHolder): boolean { function hasCode(holder: InstructionHolder): boolean {
return hasProperty(data, 'code'); return holder.code && holder.code.length > 0;
} }
function hasCall(data: InstructionHolder) { function hasCall(holder: InstructionHolder) {
return hasProperty(data, 'call'); return holder.call !== undefined;
} }
function hasProperty(object: unknown, propertyName: string) {
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
export type CategoryFactoryType = (
...parameters: ConstructorParameters<typeof Category>) => Category;
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
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;
}

View File

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

View File

@@ -1,38 +0,0 @@
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);
}
}

View File

@@ -3,26 +3,13 @@ import { ProjectInformation } from '@/domain/ProjectInformation';
import { Version } from '@/domain/Version'; import { Version } from '@/domain/Version';
export function parseProjectInformation( export function parseProjectInformation(
environment: NodeJS.ProcessEnv | VueAppEnvironment, environment: NodeJS.ProcessEnv,
): IProjectInformation { ): IProjectInformation {
const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]); const version = new Version(environment.VUE_APP_VERSION);
return new ProjectInformation( return new ProjectInformation(
environment[VueAppEnvironmentKeys.VUE_APP_NAME], environment.VUE_APP_NAME,
version, version,
environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN], environment.VUE_APP_REPOSITORY_URL,
environment[VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL], environment.VUE_APP_HOMEPAGE_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;
};

View File

@@ -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 './Validation/Syntax/SyntaxFactory'; import { SyntaxFactory } from './Syntax/SyntaxFactory';
import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory'; import { ISyntaxFactory } from './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;

View File

@@ -13,22 +13,4 @@ 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);
}
} }

View File

@@ -20,59 +20,21 @@ 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 = compileRecursively(code, context, this.extractor); const compiledCode = compileExpressions(expressions, code, context);
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 outerExpressions = expressions.filter( const sortedExpressions = expressions
(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;
@@ -103,43 +65,6 @@ 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,
@@ -155,16 +80,6 @@ function ensureParamsUsedInCodeHasArgsProvided(
} }
} }
function ensureNoInvalidIntersections(expressions: readonly IExpression[]) { function printList(list: readonly string[]): string {
const intersectingInstructions = expressions.filter( return `"${list.join('", "')}"`;
(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)}`);
}
} }

View File

@@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
.addRawRegex('([^|\\s]+)'); .addRawRegex('([^|\\s]+)');
} }
public matchMultilineAnythingExceptSurroundingWhitespaces() { public matchAnythingExceptSurroundingWhitespaces() {
return this return this
.expectZeroOrMoreWhitespaces() .expectZeroOrMoreWhitespaces()
.addRawRegex('([\\S\\s]+?)') .addRawRegex('(.+?)')
.expectZeroOrMoreWhitespaces(); .expectZeroOrMoreWhitespaces();
} }

View File

@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
.matchUntilFirstWhitespace() // First match: parameter name .matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd() .expectExpressionEnd()
// ... // ...
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text .matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }} // {{ end }}
.expectExpressionStart() .expectExpressionStart()
.expectCharacters('end') .expectCharacters('end')

View File

@@ -81,7 +81,7 @@ function compileCode(
compiler: IExpressionsCompiler, compiler: IExpressionsCompiler,
): ICompiledFunctionCall { ): ICompiledFunctionCall {
return { return {
code: compiler.compileExpressions(code.execute, args), code: compiler.compileExpressions(code.do, args),
revertCode: compiler.compileExpressions(code.revert, args), revertCode: compiler.compileExpressions(code.revert, args),
}; };
} }

View File

@@ -19,6 +19,6 @@ export enum FunctionBodyType {
} }
export interface IFunctionCode { export interface IFunctionCode {
readonly execute: string; readonly do: string;
readonly revert?: string; readonly revert?: string;
} }

View File

@@ -1,10 +1,6 @@
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( parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection;
} }

View File

@@ -1,5 +1,4 @@
import { IFunctionCall } from './Call/IFunctionCall'; import { IFunctionCall } from './Call/IFunctionCall';
import { import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody, FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
} from './ISharedFunction'; } from './ISharedFunction';
@@ -26,7 +25,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 = {
execute: code, do: code,
revert: revertCode, revert: revertCode,
}; };
return new SharedFunction(name, parameters, content, FunctionBodyType.Code); return new SharedFunction(name, parameters, content, FunctionBodyType.Code);

View File

@@ -1,9 +1,4 @@
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';
@@ -17,20 +12,16 @@ 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, syntax, this.codeValidator)) .map((func) => parseFunction(func))
.reduce((acc, func) => { .reduce((acc, func) => {
acc.addFunction(func); acc.addFunction(func);
return acc; return acc;
@@ -38,15 +29,10 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
} }
} }
function parseFunction( function parseFunction(data: FunctionData): ISharedFunction {
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
@@ -54,19 +40,6 @@ function parseFunction(
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) => {

View File

@@ -1,10 +1,6 @@
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 } from '@/domain/ScriptCode'; import { ScriptCode, ILanguageSyntax } 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';
@@ -12,20 +8,18 @@ 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,
syntax: ILanguageSyntax, private readonly syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
) { ) {
if (!syntax) { throw new Error('missing syntax'); } if (!syntax) { throw new Error('missing syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax); this.functions = sharedFunctionsParser.parseFunctions(functions);
} }
public canCompile(script: ScriptData): boolean { public canCompile(script: ScriptData): boolean {
@@ -41,19 +35,13 @@ 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()]),
);
}

View File

@@ -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;

View File

@@ -1,41 +1,26 @@
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 { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator'; import { parseDocUrls } from '../DocumentationParser';
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 {
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data }); validateScript(data);
validateScript(data, validator);
if (!context) { throw new Error('missing context'); } if (!context) { throw new Error('missing context'); }
try { const script = new Script(
const script = scriptFactory( /* name: */ data.name,
/* name: */ data.name, /* code: */ parseCode(data, context),
/* code: */ parseCode(data, context, codeValidator), /* docs: */ parseDocUrls(data),
/* docs: */ parseDocs(data), /* level: */ parseLevel(data.recommend, levelParser),
/* level: */ parseLevel(data.recommend, levelParser), );
); return script;
return script;
} catch (err) {
validator.throw(err.message);
}
} }
function parseLevel( function parseLevel(
@@ -48,50 +33,28 @@ function parseLevel(
return parser.parseEnum(level, 'level'); return parser.parseEnum(level, 'level');
} }
function parseCode( function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
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);
} }
const code = new ScriptCode(script.code, script.revertCode); return new ScriptCode(script.code, script.revertCode, context.syntax);
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
return code;
} }
function validateHardcodedCodeWithoutCalls( function ensureNotBothCallAndCode(script: ScriptData) {
scriptCode: ScriptCode, if (script.code && script.call) {
codeValidator: ICodeValidator, throw new Error('cannot define both "call" and "code"');
syntax: ILanguageSyntax, }
) { if (script.revertCode && script.call) {
[scriptCode.execute, scriptCode.revert].forEach( throw new Error('cannot define "revertCode" if "call" is defined');
(code) => codeValidator.throwIfInvalid( }
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
} }
function validateScript(script: ScriptData, validator: NodeValidator) { function validateScript(script: ScriptData) {
validator if (!script) {
.assertDefined(script) throw new Error('missing script');
.assertValidName(script.name) }
.assert( if (!script.code && !script.call) {
() => Boolean(script.code || script.call), throw new Error('must define either "call" or "code"');
'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);

View File

@@ -1,4 +1,4 @@
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { ILanguageSyntax } from '@/domain/ScriptCode';
const BatchFileCommonCodeParts = ['(', ')', 'else', '||']; const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
const PowerShellCommonCodeParts = ['{', '}']; const PowerShellCommonCodeParts = ['{', '}'];

View File

@@ -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>;

View File

@@ -0,0 +1,7 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = ['#'];
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
}

View File

@@ -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';

View File

@@ -1,46 +0,0 @@
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');
}

View File

@@ -1,4 +0,0 @@
export interface ICodeLine {
readonly index: number;
readonly text: string;
}

View File

@@ -1,10 +0,0 @@
import { ICodeLine } from './ICodeLine';
export interface IInvalidCodeLine {
readonly index: number;
readonly error: string;
}
export interface ICodeValidationRule {
analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[];
}

View File

@@ -1,8 +0,0 @@
import { ICodeValidationRule } from './ICodeValidationRule';
export interface ICodeValidator {
throwIfInvalid(
code: string,
rules: readonly ICodeValidationRule[],
): void;
}

View File

@@ -1,47 +0,0 @@
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();
}

View File

@@ -1,21 +0,0 @@
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}"`;
})(),
}));
}
}

View File

@@ -1,4 +0,0 @@
export interface ILanguageSyntax {
readonly commentDelimiters: string[];
readonly commonCodeParts: string[];
}

View File

@@ -1,7 +0,0 @@
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = ['#'];
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
}

View File

@@ -12,10 +12,10 @@ declare module '@/application/collections/*' {
} }
export type CategoryOrScriptData = CategoryData | ScriptData; export type CategoryOrScriptData = CategoryData | ScriptData;
export type DocumentationData = ReadonlyArray<string> | string; export type DocumentationUrlsData = ReadonlyArray<string> | string;
export interface DocumentableData { export interface DocumentableData {
readonly docs?: DocumentationData; readonly docs?: DocumentationUrlsData;
} }
export interface InstructionHolder { export interface InstructionHolder {

File diff suppressed because it is too large Load Diff

View File

@@ -510,81 +510,35 @@ actions:
parameters: parameters:
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1 configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
- -
category: Configure Parallels Desktop category: Configure Parallels
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: children:
- -
name: Turn off ads in Parallels Desktop name: Turn off ads in Parallels
recommend: standard docs: https://hints.macworld.com/article.php?story=20120724235352514
docs: |- # Check: defaults read 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff'
Parallels Desktop in-product notifications to show ads from Parallels or other third code: defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes
party companies [1]. # Default: 0 (no)
revertCode: defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool no
The main setting is `ProductPromo.ForcePromoOff` [1] that you can check using: # There's more settings but they're off (has value "1" by default):
# WelcomeScreenPromo.PromoOff (default 1)
1. `defaults read 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff'` # NotificationPromo.6635.PromoOff (default 1)
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 category: Disable Parallels 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: children:
- -
name: Disable automatically downloading Parallels Desktop updates name: Disable automatically downloading Parallels updates
docs: |- docs: https://download.parallels.com/desktop/v17/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm
Automatic downloads are enabled by default, and this script disables automatic downloads. # Check: defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically'
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 code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool no
# Default: 1 (enabled)
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool yes revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool yes
- -
name: Disable automatically checking for Parallels Desktop updates name: Disable automatically checking for Parallels updates
docs: |- docs: https://download.parallels.com/desktop/v17/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm
Automatic checks are weekly by default, and this script disables the checks completely. # Check: defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates'
# Values: 0 - Never, 1 - Once a day, 2 - Once a week, 3 - Once a month
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 code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 0
# Default: 2 (once a week)
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 2 revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 2
- -
category: Configure OS category: Configure OS
@@ -716,57 +670,51 @@ actions:
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 category: Configure crash reporting (quit dialog after an application crash)
recommend: standard # Prompts for sending data to Apple
docs: |- children:
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, name: Disable Crash reporting
which prevents Apple's advertising platform from using your personal information to deliver targeted # Quit dialog after an application crash
ads [1]. # "The application <application> has unexpectedly quit" alert
docs:
When Personalized Ads is enabled, your information may be used to provide ads that closely align # Since 10.4 (Basic, Developer, and Server)
with your interests [1]. You might occasionally encounter such targeted ads in Apple News, Stocks, - https://web.archive.org/web/20090411195107/http://developer.apple.com/qa/qa2001/qa1288.html
and the Mac App Store [2]. Disabling Personalized Ads will prevent Apple from using your data for # Before 10.4
ad targeting [2]. Although this does not necessarily decrease the quantity of ads you receive, # "none" = Don't show any dialog at all (crash reports are still silently written to disk)
it may result in the ads being less relevant to your interests [2]. # "prompt" = show the unexpectedly quit dialog and prompt to see if the user wants to submit/view the crashreport
# "crashreport" = don't show the unexpectedly quit dialog - instead immediately show the crashreport/submission screen.
The primary keys to deactivating personalized ads are: - https://web.archive.org/web/20040816171016/http://developer.apple.com/qa/qa2001/qa1288.html
# Description for crashreporter preferences (basic, developer, server), stating basic is the default one
- **`allowApplePersonalizedAdvertising`**: If set to false, this restricts Apple's personalized # https://web.archive.org/web/20090228102631/http://developer.apple.com/technotes/tn2004/tn2123.html#SECCRASHREPORTERPREFS
advertising [3]. This is applicable on macOS 12 and subsequent versions [3]. # Check: defaults read 'com.apple.CrashReporter' 'DialogType'
- **`allowIdentifierForAdvertising`**: The `advertisingIdentifier` is a unique string assigned # Values: none|basic (default)|developer|server (before 10.4: crashreport|none)
to each device [5]. Apple uses this identifier and recommends its use in third-party code: defaults write 'com.apple.CrashReporter' 'DialogType' -string 'none'
applications for tasks like frequency capping, attribution, conversion events, estimating the revertCode: |- # TODO: Or delete? Since monterey com.apple.CrashReporter is empty
number of unique users, detecting advertising fraud, and debugging [5]. Although there is no os_major_ver=$(sw_vers -productVersion | awk -F "." '{print $1}')
official documentation on it, a discussion on JAMF.com corroborates its existence [6]. os_minor_ver=$(sw_vers -productVersion | awk -F "." '{print $2}')
# Older (before 10.4): prompt|crashreport
My tests show that disabling any of the keys mentioned above results in the if [[ $os_major_ver -le 10 \
"System Preferences > Apple Advertising > Personalized ads" option being deactivated in the GUI, || ( $os_major_ver -eq 10 && $os_minor_ver -le 4 ) \
starting from macOS Monterey. ]]; then
defaults write 'com.apple.CrashReporter' 'DialogType' -string 'prompt'
Please note: The `forceLimitAdTracking` key limits ad tracking [3] [4] and is found in CIS else
benchmarks for macOS [4]. However, the official macOS documentation specifies that it is # Newer (since 10.4): basic|developer|server
applicable only to iOS 7 and later versions, not to macOS [3]. The key does not exist on the OS defaults write 'com.apple.CrashReporter' 'DialogType' -string 'basic'
by default. fi
-
[1]: https://web.archive.org/web/20230731152633/https://www.apple.com/legal/privacy/data/en/apple-advertising/ "Legal - Apple Advertising & Privacy - Apple" name: Use notification instead of report after crash
[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)" docs:
[3]: https://web.archive.org/web/20230731155827/https://developer.apple.com/documentation/devicemanagement/restrictions "Restrictions | Apple Developer Documentation" # Removing the Crash Reporter may be overkill for some users, so another option is to
[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" # change this to a notification instead.
[5]: https://web.archive.org/web/20230731155131/https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier "advertisingIdentifier | Apple Developer Documentation" # The advantage of this is you still get notified if an app has crashed, but you don't
[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' # have to respond in any way (nothing to click on). To undo this change you would use the
code: |- # same entry but replace the 1 with a 0.
defaults write com.apple.AdLib allowIdentifierForAdvertising -bool false - https://www.defaults-write.com/os-x-make-crash-reporter-appear-as-a-notification/
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool false - https://osxdaily.com/2015/10/13/set-crash-reporter-as-notification-mac-os-x/
defaults write com.apple.AdLib forceLimitAdTracking -bool true code: defaults write 'com.apple.CrashReporter' 'UseUNC' 1
# Default: (`defaults read com.apple.AdLib`) revertCode: |- # TODO: Or delete? Since monterey com.apple.CrashReporter is empty
# - `defaults read com.apple.AdLib allowApplePersonalizedAdvertising`: true (1) defaults write 'com.apple.CrashReporter' 'UseUNC' 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:
@@ -1205,6 +1153,106 @@ actions:
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
# Trigger background check with normal scan (critical updates only) # Trigger background check with normal scan (critical updates only)
sudo softwareupdate --background-critical sudo softwareupdate --background-critical
-
category: UI for privacy
children:
-
name: Show hidden files in Finder
recommend: strict # Because NIST recommends it
docs:
# Disables hiding files `.htaccess` in Finder
- https://macos-defaults.com/finder/appleshowallfiles.html
# Securing Apple OSX X 10.10 Systems: NIST Security Configuration Checklist:
# Finder should be configured to not show hidden files and folders; this is already configured by
# default. Finder should also be configured to show file extensions, to show a warning before
# changing a file extension or emptying the trash, and to search this system when performing a
# search. Administrators with intimate knowledge of the OS X system could notice unusual hidden
# files and would benefit from their visibility. Consequently, hidden files should be displayed in an
# SSLF environment. These options can improve defenses against malware. To configure these
# options, go to Finder / Preferences / Advanced; then enable the corresponding options.
- https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-179.pdf
# Check: defaults read 'com.apple.finder' 'AppleShowAllFiles' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: |-
killall cfprefsd
defaults write 'com.apple.finder' 'AppleShowAllFiles' -bool true
killall 'Finder'
revertCode: |-
killall cfprefsd
defaults delete 'com.apple.finder' 'AppleShowAllFiles'
killall 'Finder'
-
name: Hide Desktop icons
docs: https://www.defaults-write.com/os-x-how-to-quickly-hide-the-desktop-icons/
# When doing presentations, it can be useful to hide desktop icons
# Check: defaults read 'com.apple.finder' 'CreateDesktop' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: |-
killall cfprefsd
defaults write 'com.apple.finder' 'CreateDesktop' -bool false
killall Finder
revertCode: |-
killall cfprefsd
defaults delete 'com.apple.finder' 'CreateDesktop'
killall Finder
# TODO: https://github.com/mathiasbynens/dotfiles/blob/main/.macos
# defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false
# defaults write com.apple.finder ShowHardDrivesOnDesktop -bool false
# defaults write com.apple.finder ShowMountedServersOnDesktop -bool false
# defaults write com.apple.finder ShowRemovableMediaOnDesktop -bool false
-
name: Show all filename extensions # TODO: docs from https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-179.pdf
# Check: defaults read 'NSGlobalDomain' 'AppleShowAllExtensions' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: |-
killall cfprefsd
defaults write 'NSGlobalDomain' 'AppleShowAllExtensions' -bool true
killall Finder
revertCode: |-
killall cfprefsd
defaults delete 'NSGlobalDomain' 'AppleShowAllExtensions'
killall Finder
-
name: show path bar
# TODO: defaults write com.apple.finder ShowPathbar -bool true
-
category: Disable creation of metadata files (`.DS_Store`) # TODO: A better category
# macOS creates metadata files as and when files are saved to the hard drive.
# These metadata files can also be viewed further giving malicious actors the extra edge.
# It is recommended that Mac OS X users disable creation of Metadata Files to further boost the
# privacy levels.
children:
-
name: Disable metadata files (`.DS_Store`) on Network Volumes
recommend: strict
docs:
- https://support.apple.com/en-us/HT208209
- https://web.archive.org/web/20190919161732/https://support.apple.com/en-gb/HT1629
# Check: defaults read 'com.apple.desktopservices' 'DSDontWriteNetworkStores' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: defaults write 'com.apple.desktopservices' 'DSDontWriteNetworkStores' -bool true
revertCode: defaults delete 'com.apple.desktopservices' 'DSDontWriteNetworkStores'
-
name: Disable metadata files (`.DS_Store`) on USB Volumes
recommend: strict
docs: https://krypted.com/mac-security/disable-dsstore-files-on-usb-drives/
# Check: defaults read 'com.apple.desktopservices' 'DSDontWriteUSBStores' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: defaults write 'com.apple.desktopservices' 'DSDontWriteUSBStores' -bool true
revertCode: defaults delete 'com.apple.desktopservices' 'DSDontWriteUSBStores'
-
name: Clear created metadata files # TODO: Not tested
code: |- # "fstype local to search only on the local mounted file system and ignore any other mounted disk under
find ~ \
-type f \
-name .DS_Store \
-fstype local \
-exec \
sh -c \
'
file="{}"
rm -fv "$file"
' \
{} \;
functions: functions:
- -
name: PersistUserEnvironmentConfiguration name: PersistUserEnvironmentConfiguration
@@ -1235,3 +1283,9 @@ functions:
echo "[$profile_file] No need for any action, configuration does not exist" echo "[$profile_file] No need for any action, configuration does not exist"
fi fi
done done
# TODO:
# Disable disk image verification (or do enable)
# defaults write com.apple.frameworks.diskimages skip-verify -bool true
# defaults write com.apple.frameworks.diskimages skip-verify-locked -bool true
# defaults write com.apple.frameworks.diskimages skip-verify-remote -bool true

File diff suppressed because it is too large Load Diff

View File

@@ -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 docs: ReadonlyArray<string>, public readonly documentationUrls: ReadonlyArray<string>,
public readonly subCategories?: ReadonlyArray<ICategory>, public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts?: ReadonlyArray<IScript>, public readonly scripts?: ReadonlyArray<IScript>,
) { ) {

View File

@@ -1,3 +1,3 @@
export interface IDocumentable { export interface IDocumentable {
readonly docs: ReadonlyArray<string>; readonly documentationUrls: ReadonlyArray<string>;
} }

View File

@@ -4,8 +4,6 @@ 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;

View File

@@ -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 docs: ReadonlyArray<string>; readonly documentationUrls: ReadonlyArray<string>;
readonly code: IScriptCode; readonly code: IScriptCode;
canRevert(): boolean; canRevert(): boolean;
} }

View File

@@ -9,7 +9,6 @@ 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,
) { ) {
@@ -19,9 +18,6 @@ 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');
} }

View File

@@ -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 docs: ReadonlyArray<string>, public readonly documentationUrls: ReadonlyArray<string>,
public readonly level?: RecommendationLevel, public readonly level?: RecommendationLevel,
) { ) {
super(name); super(name);
if (!code) { if (!code) {
throw new Error('missing code'); throw new Error(`missing code (script: ${name})`);
} }
validateLevel(level); validateLevel(level);
} }

View File

@@ -4,18 +4,25 @@ 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,
) { ) {
validateCode(execute); if (!syntax) { throw new Error('missing syntax'); }
validateRevertCode(revert, execute); validateCode(execute, syntax);
validateRevertCode(revert, execute, syntax);
} }
} }
function validateRevertCode(revertCode: string, execute: string) { export interface ILanguageSyntax {
readonly commentDelimiters: string[];
readonly commonCodeParts: string[];
}
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
if (!revertCode) { if (!revertCode) {
return; return;
} }
try { try {
validateCode(revertCode); validateCode(revertCode, syntax);
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');
} }
@@ -24,8 +31,54 @@ function validateRevertCode(revertCode: string, execute: string) {
} }
} }
function validateCode(code: string): void { function validateCode(code: string, syntax: ILanguageSyntax): 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();
} }

View File

@@ -26,8 +26,6 @@ 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

View File

@@ -1,6 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 302 B

View File

@@ -4,19 +4,18 @@
@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;
@include hover-or-touch { &:hover {
color: $globals-color-hover; color: $color-primary;
} }
} }

View File

@@ -1,29 +0,0 @@
@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;
}

View File

@@ -4,7 +4,6 @@
@forward "./media"; @forward "./media";
@forward "./colors"; @forward "./colors";
@forward "./globals"; @forward "./globals";
@forward "./mixins";
@forward "./components/card"; @forward "./components/card";

View File

@@ -1,6 +1,5 @@
// 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;
@@ -19,17 +18,14 @@ $color-node-checkbox-tick-checked : $color-on-secondary;
&-node { &-node {
white-space: normal !important; white-space: normal !important;
> .tree-content { > .tree-content {
> .tree-anchor { > .tree-anchor > span {
> span { color: $color-node-fg;
color: $color-node-fg; text-transform: uppercase;
font-size: 1.5em; font-size: 1.5em;
}
display: block; // so it takes full width to allow aligning items inside
} }
@include hover-or-touch { &:hover {
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;

View File

@@ -28,14 +28,13 @@ 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;
align-items: center; padding-left: 0.3rem;
padding: 0.2rem; padding-right: 0.3rem;
.dollar { .dollar {
margin-right: 0.5rem; margin-right: 0.5rem;
font-size: 0.8rem; font-size: 0.8rem;
@@ -43,13 +42,13 @@ export default class Code extends Vue {
} }
.copy-button { .copy-button {
margin-left: 1rem; margin-left: 1rem;
@include clickable; cursor: pointer;
@include hover-or-touch { &:hover {
color: $color-primary; color: $color-primary;
} }
} }
code { code {
font-size: 1rem; font-size: 1.2rem;
} }
} }
</style> </style>

View File

@@ -42,18 +42,17 @@ 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;
@include clickable; cursor: pointer;
width: 10%; width: 10%;
min-width: 90px; min-width: 90px;
@include hover-or-touch { &:hover {
background: $color-surface; background: $color-surface;
box-shadow: 0px 2px 10px 5px $color-secondary; box-shadow: 0px 2px 10px 5px $color-secondary;
} }
@include hover-or-touch('>&__text') { &:hover>&__text {
display: block; display: block;
} }
@include hover-or-touch('>&__icon') { &:hover>&__icon {
display: none; display: none;
} }
&__text { &__text {
@@ -63,9 +62,6 @@ 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>

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