Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50ba00b0af | ||
|
|
29e1069bf2 | ||
|
|
c7e57b8913 | ||
|
|
4cea6b26ec | ||
|
|
c2f4b68786 | ||
|
|
e8add5ec08 | ||
|
|
55c23e9d4c | ||
|
|
d77c3cbbe2 | ||
|
|
f89c2322b0 | ||
|
|
ded55a66d6 | ||
|
|
6fbc81675f | ||
|
|
48d97afdf6 | ||
|
|
109fc01c9a | ||
|
|
b185255a0a | ||
|
|
c2d3cddc47 | ||
|
|
8526d2510b | ||
|
|
11e566d0e5 | ||
|
|
ae0165f1fe | ||
|
|
a6505587bf | ||
|
|
b16e13678c | ||
|
|
abe03cef3f | ||
|
|
dd7239b8c1 | ||
|
|
851917e049 | ||
|
|
8d7a7eb434 | ||
|
|
0239b52385 | ||
|
|
19ea8dbc5b | ||
|
|
70959ccada | ||
|
|
5d365f65fa | ||
|
|
cca397c8c7 | ||
|
|
1430d5215a | ||
|
|
c09c5ffa47 | ||
|
|
ed7e69c07e | ||
|
|
f286f92b1f | ||
|
|
e7031a3ae4 | ||
|
|
2f828735a8 | ||
|
|
78c62cfc95 | ||
|
|
ed93614ca3 | ||
|
|
fac26a6ca0 | ||
|
|
48761f62a2 | ||
|
|
dc03bff324 | ||
|
|
e9a52859f6 | ||
|
|
1a10cf2e5f | ||
|
|
1c2d82dc9b | ||
|
|
6ecfa9b954 | ||
|
|
c138f74460 | ||
|
|
8becc7dbc4 | ||
|
|
b29cd7b5f7 | ||
|
|
f21ef9250a | ||
|
|
fa2a92bf89 | ||
|
|
8341411be4 | ||
|
|
22d6c7991e | ||
|
|
795b7f0321 | ||
|
|
9e34e64449 | ||
|
|
ce4cfdd169 | ||
|
|
12b1f183f7 | ||
|
|
4212c7b9e0 | ||
|
|
7794846185 | ||
|
|
150e067039 | ||
|
|
f347fde0c8 | ||
|
|
ff3d5c4841 | ||
|
|
292362135d | ||
|
|
aae5434451 | ||
|
|
2390530d92 | ||
|
|
9ab3ff75b0 | ||
|
|
d25c4e8c81 | ||
|
|
4a7efa27c8 | ||
|
|
cec0b4b4f6 | ||
|
|
a1922c50c1 | ||
|
|
870120bc13 | ||
|
|
f38cf73485 | ||
|
|
9fd193e676 | ||
|
|
52a4730073 | ||
|
|
bc4879cfe9 | ||
|
|
dd71536316 | ||
|
|
a3343205b1 | ||
|
|
1d7cafc831 | ||
|
|
c75df1c8c1 | ||
|
|
ab25e0a066 | ||
|
|
813d820b85 | ||
|
|
66a56888a4 | ||
|
|
4ef16cea56 | ||
|
|
8c17396285 | ||
|
|
694bf1a74d | ||
|
|
0fc2ffc1ea | ||
|
|
d19dde603d | ||
|
|
23bac0fc76 | ||
|
|
e18907ca91 | ||
|
|
4e21f05031 | ||
|
|
8b224eefe7 | ||
|
|
f261ab4cd9 | ||
|
|
f584fabb50 | ||
|
|
2eed6f4afb | ||
|
|
1c9dc93246 |
57
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report (script bug or unexpected script behavior)
|
|
||||||
about: Create a bug report for generated scripts to help privacy.sexy improve
|
|
||||||
labels: bug
|
|
||||||
title: '[BUG]: '
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for reporting an issue with generated script(s).
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### OS
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Which OS are you using? What version of OS you were using?
|
|
||||||
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
|
||||||
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
|
||||||
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Reproduction steps
|
|
||||||
|
|
||||||
<!--
|
|
||||||
How can the bug be recreated?
|
|
||||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
|
||||||
E.g.
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional information
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context about the problem here.
|
|
||||||
-->
|
|
||||||
114
.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
name: "Bug Report: Script Issues"
|
||||||
|
description: 🐛 Report issues with generated scripts to enhance privacy.sexy
|
||||||
|
labels: [ 'bug' ]
|
||||||
|
title: '[Bug]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "After running the cleanup script, music playback stopped functioning."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: How can the bug be recreated?
|
||||||
|
description: |-
|
||||||
|
This is the most important information in the bug report.
|
||||||
|
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||||
|
placeholder: |-
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: |-
|
||||||
|
Please specify your operating system and its version.
|
||||||
|
|
||||||
|
- On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||||
|
- On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||||
|
- On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Windows 11 Pro 22H3"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Script file
|
||||||
|
description: |-
|
||||||
|
If applicable, share the generated privacy.sexy file.
|
||||||
|
|
||||||
|
GitHub may restrict script file attachments.
|
||||||
|
Upload your script file to a service like [GitHub Gist](https://gist.github.com/) and share the link here.
|
||||||
|
|
||||||
|
If you used the desktop version to run the script, it is already stored on your system.
|
||||||
|
See the [documentation to locate it](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#secure-script-executionstorage).
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach script files by dragging them into this area.
|
||||||
|
placeholder: |-
|
||||||
|
Attach the script, or post GitHub Gist link.
|
||||||
|
For example: https://gist.github.com/privacysexy-forks/6d85ad8ca27acc8c6a5417d4af28c9b6.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: |-
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||||
|
placeholder: Attach screenshots here or link to image hosting.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
|
||||||
|
Helpful information includes:
|
||||||
|
|
||||||
|
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||||
|
- Terminal output
|
||||||
|
- Proposed solutions
|
||||||
|
- Other related context such as related issues, software behavior, etc.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
104
.github/ISSUE_TEMPLATE/2-bug-report-general.yaml
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
name: "Bug Report: General"
|
||||||
|
description: 🐛 Report general issues to enhance privacy.sexy
|
||||||
|
labels: [ 'bug' ]
|
||||||
|
title: '[Bug]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Provide a clear and concise description of the issue.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "I cannot select any scripts."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction steps
|
||||||
|
description: |-
|
||||||
|
This is the most important information in the bug report.
|
||||||
|
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||||
|
placeholder: |-
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Describe what you expected to happen when the error occurred.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "I expected the settings menu to open smoothly without crashing.".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: |-
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||||
|
placeholder: >-
|
||||||
|
Attach screenshots here or link to image hosting.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: privacy.sexy environment details
|
||||||
|
description: |-
|
||||||
|
If applicable, mention how you were using privacy.sexy when the bug occurred:
|
||||||
|
|
||||||
|
- Web (on which operating system and browser?)
|
||||||
|
- Or desktop (Windows, macOS, or Linux?)
|
||||||
|
placeholder: >-
|
||||||
|
For example: "The web version on Edge browser on Windows 11 23H2."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
|
||||||
|
Helpful information includes:
|
||||||
|
|
||||||
|
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||||
|
- Terminal output
|
||||||
|
- Proposed solutions
|
||||||
|
- Other related context such as related issues, software behavior, etc.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
55
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report (unrelated to generated scripts)
|
|
||||||
about: Create a bug report that's not related to generated scripts to help privacy.sexy improve
|
|
||||||
labels: bug
|
|
||||||
title: '[BUG]: '
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for reporting an issue.
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Reproduction steps
|
|
||||||
|
|
||||||
<!--
|
|
||||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Distribution
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
|
||||||
- Web (on Desktop or mobile?)
|
|
||||||
- Or desktop (Windows, macOS or Linux?)
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context about the problem here.
|
|
||||||
-->
|
|
||||||
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for suggesting an idea to improve privacy better 🤗.
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Problem description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
What are we trying to solve?
|
|
||||||
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
|
||||||
E.g. I'm always frustrated when [...]
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Proposed solution
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe the solution you'd like in a clear and concise manner.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Alternatives considered
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional information
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context or screenshots about the feature request here.
|
|
||||||
-->
|
|
||||||
73
.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: "Suggestion: Feature"
|
||||||
|
description: 💡 Suggest new ideas to enhance privacy.sexy
|
||||||
|
labels: [ 'enhancement' ]
|
||||||
|
title: '[Feature]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem statement
|
||||||
|
description: |-
|
||||||
|
What are we trying to solve?
|
||||||
|
|
||||||
|
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Every time I use the app, I struggle with..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
description: |-
|
||||||
|
Describe the solution you'd like in a clear and concise manner.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "It would be great if the app could..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: |-
|
||||||
|
Have you considered any alternative solutions or features?
|
||||||
|
Different perspectives can inspire new ideas.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "We could also solve it by...".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context or screenshots about the feature request here.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach files or screenshots by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Challenges can be ..., but I'm unsure about ..., here is some documentation about it: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
name: New script suggestion
|
|
||||||
about: Suggest a new script for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for contributing to privacy.sexy! 🌟
|
|
||||||
For guidance, see our script guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md.
|
|
||||||
Consider submitting a PR for faster implementation: https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Operating system
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Specify the OS: Windows, macOS, or Linux.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Name
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a name for the script.
|
|
||||||
Naming conventions: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Code
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide or explain the code to execute when the script runs.
|
|
||||||
Code guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Revert code
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Include code to revert changes to the default state.
|
|
||||||
Leave blank for non-reversible scripts.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Category
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a category for the script.
|
|
||||||
If unsure, leave blank for maintainers to decide.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Recommendation level
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a recommendation level: STANDARD (non-breaking), STRICT (limits functionality), or NONE (for advanced users).
|
|
||||||
If unsure, leave blank for maintainers to decide.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Documentation/References
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide any relevant documentation or references.
|
|
||||||
Prefer high-quality sources such as vendor documentation.
|
|
||||||
Documentation guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation.
|
|
||||||
-->
|
|
||||||
133
.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
name: "Suggestion: New Script"
|
||||||
|
description: 💡 Suggest new scripts to enhance privacy.sexy
|
||||||
|
labels: [ 'enhancement' ]
|
||||||
|
title: '[New script]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
|
||||||
|
For guidance, see our [script guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md).
|
||||||
|
Consider submitting a PR to get your script added more quickly: (see [CONTRIBUTING.md](https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts))
|
||||||
|
-
|
||||||
|
type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: Which operating system will the new script configure?
|
||||||
|
options:
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- All of them
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Name of the script
|
||||||
|
description: |-
|
||||||
|
Suggest a name for the script that clearly describes its function.
|
||||||
|
|
||||||
|
See [script naming conventions](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name) for best practices.
|
||||||
|
placeholder: E.g, "Disable error data submission"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Documentation/References
|
||||||
|
description: |-
|
||||||
|
Provide any relevant documentation or references.
|
||||||
|
Prefer high-quality sources such as vendor documentation.
|
||||||
|
|
||||||
|
See [documentation guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation) for best practices.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "This script will disable the error data submission, see https://microsoft.com/...".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Code
|
||||||
|
description: |-
|
||||||
|
If possible, provide or explain the code that the script should execute.
|
||||||
|
|
||||||
|
See [script code guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code).
|
||||||
|
placeholder: |-
|
||||||
|
For example: "Set registry key like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "1"`".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Revert code
|
||||||
|
description: |-
|
||||||
|
If applicable, provide revert code to restore the changes made by the script.
|
||||||
|
|
||||||
|
The revert code restores changes to their default state before script execution.
|
||||||
|
|
||||||
|
Leave blank for non-reversible scripts.
|
||||||
|
placeholder: |-
|
||||||
|
For example: "Revert to operating system default like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "0"`".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Suggested category
|
||||||
|
description: |-
|
||||||
|
Suggest a category for the script.
|
||||||
|
|
||||||
|
If unsure, leave blank for maintainers to decide.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Privacy Cleanup > Clear system logs"
|
||||||
|
-
|
||||||
|
type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Recommendation level
|
||||||
|
description: |-
|
||||||
|
Suggest a recommendation level for the script:
|
||||||
|
|
||||||
|
- **Standard**: Recommended for most users without side-effects.
|
||||||
|
- **Strict**: Provides improved privacy at the cost of some functionality.
|
||||||
|
- **None**: For advanced users or specific needs.
|
||||||
|
|
||||||
|
If unsure, leave blank for maintainers to decide.
|
||||||
|
options:
|
||||||
|
- Standard
|
||||||
|
- Strict
|
||||||
|
- None (do not recommend)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context or screenshots about the script request here.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach additional documents or screenshots by dragging them into this area or pasting directly.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Challenges can be ..., but I am unsure about ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,7 @@
|
|||||||
|
# This file must be named `config.yml`. GitHub does not recognize the file if it is named `config.yaml`.
|
||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Donate
|
||||||
|
url: https://undergroundwires.dev/donate/
|
||||||
|
about: ❤️ Donate to support the free software you love to keep it alive.
|
||||||
|
# A separate link for reporting vulnerabilities is not included here because GitHub generates it automatically.
|
||||||
|
|||||||
2
.github/actions/setup-node/action.yml
vendored
@@ -6,4 +6,4 @@ runs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
check-latest: true
|
# check-latest: true # Newest versions can potentially have undiscovered bugs or regressions
|
||||||
|
|||||||
15
.github/actions/upload-artifact/action.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
required: true
|
||||||
|
path:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.name }}
|
||||||
|
path: ${{ inputs.path }}
|
||||||
9
.github/workflows/checks.build.yaml
vendored
@@ -72,16 +72,19 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
|
os:
|
||||||
|
- macos-13 # Downgraded due to lack of nested virtualization support in ARM-based runners (See: actions/runner-images#9460, actions/runner-images#9741, abiosoft/colima#1023)
|
||||||
|
- ubuntu-latest
|
||||||
|
# - windows-latest # Windows runners do not support Linux containers
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Install Docker on macOS
|
name: Install Docker on macOS
|
||||||
if: matrix.os == 'macos' # macOS runner is missing Docker
|
if: contains(matrix.os, 'macos') # macOS runner is missing Docker
|
||||||
run: |-
|
run: |-
|
||||||
# Install Docker
|
# Install Docker
|
||||||
brew install docker
|
brew install docker
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ jobs:
|
|||||||
run-check:
|
run-check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu, windows ]
|
os:
|
||||||
|
- macos-latest # Apple silicon (ARM64)
|
||||||
|
- macos-13 # Intel-based (x86-64)
|
||||||
|
- ubuntu-latest
|
||||||
|
- windows-latest
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -24,7 +28,7 @@ jobs:
|
|||||||
uses: ./.github/actions/npm-install-dependencies
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Configure Ubuntu
|
name: Configure Ubuntu
|
||||||
if: matrix.os == 'ubuntu'
|
if: contains(matrix.os, 'ubuntu') # macOS runner is missing Docker
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update
|
sudo apt update
|
||||||
@@ -66,7 +70,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Upload screenshot
|
name: Upload screenshot
|
||||||
if: always() # Run even if previous step fails
|
if: always() # Run even if previous step fails
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: screenshot-${{ matrix.os }}
|
name: screenshot-${{ matrix.os }}
|
||||||
path: screenshot.png
|
path: screenshot.png
|
||||||
|
|||||||
75
.github/workflows/checks.quality.yaml
vendored
@@ -1,10 +1,10 @@
|
|||||||
name: quality-checks
|
name: checks.quality
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
lint-command:
|
lint-command:
|
||||||
@@ -28,3 +28,74 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Lint
|
name: Lint
|
||||||
run: ${{ matrix.lint-command }}
|
run: ${{ matrix.lint-command }}
|
||||||
|
|
||||||
|
todo-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Scan latest commit for TODO comments
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
readonly todo_comment_search_pattern='TODO'':' # Define search pattern in parts to prevent IDE from flagging this script line as a TODO item
|
||||||
|
if git grep "$todo_comment_search_pattern" HEAD; then
|
||||||
|
echo 'TODO comments found in the latest commit.'
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo 'No TODO comments found in the latest commit.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pylint:
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pylint
|
||||||
|
-
|
||||||
|
name: Analyzing the code with pylint
|
||||||
|
run: npm run lint:pylint
|
||||||
|
|
||||||
|
validate-collection-files:
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
||||||
|
-
|
||||||
|
name: Validate
|
||||||
|
run: python3 ./scripts/validate-collections-yaml
|
||||||
|
|||||||
32
.github/workflows/checks.scripts.yaml
vendored
@@ -15,6 +15,10 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Install ImageMagick on macOS
|
||||||
|
if: matrix.os == 'macos'
|
||||||
|
run: brew install imagemagick
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -53,3 +57,31 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run install-deps
|
name: Run install-deps
|
||||||
run: ${{ matrix.install-command }}
|
run: ${{ matrix.install-command }}
|
||||||
|
|
||||||
|
configure-vscode:
|
||||||
|
runs-on: ${{ matrix.os.name }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- name: macos
|
||||||
|
install-vscode-command: brew install --cask visual-studio-code
|
||||||
|
- name: ubuntu
|
||||||
|
install-vscode-command: sudo snap install code --classic
|
||||||
|
- name: windows
|
||||||
|
install-vscode-command: choco install vscode
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
-
|
||||||
|
name: Install VSCode
|
||||||
|
run: ${{ matrix.os.install-vscode-command }}
|
||||||
|
-
|
||||||
|
name: Configure VSCode
|
||||||
|
run: python3 ./scripts/configure_vscode.py
|
||||||
|
|||||||
4
.github/workflows/tests.e2e.yaml
vendored
@@ -51,14 +51,14 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Upload screenshots
|
name: Upload screenshots
|
||||||
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: e2e-screenshots-${{ matrix.os }}
|
name: e2e-screenshots-${{ matrix.os }}
|
||||||
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
||||||
-
|
-
|
||||||
name: Upload videos
|
name: Upload videos
|
||||||
if: always() # Run even if previous steps fail because test run video is always captured
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: e2e-videos-${{ matrix.os }}
|
name: e2e-videos-${{ matrix.os }}
|
||||||
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -14,3 +14,7 @@ node_modules
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
.venv
|
||||||
4
.vscode/extensions.json
vendored
@@ -5,8 +5,10 @@
|
|||||||
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
||||||
"wayou.vscode-todo-highlight", // Highlights TODO.
|
"wayou.vscode-todo-highlight", // Highlights TODO.
|
||||||
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
||||||
// Documentation
|
// Markdown
|
||||||
"davidanson.vscode-markdownlint", // Lints markdown.
|
"davidanson.vscode-markdownlint", // Lints markdown.
|
||||||
|
// YAML
|
||||||
|
"redhat.vscode-yaml", // Lints YAML files, validates against schema.
|
||||||
// TypeScript / JavaScript
|
// TypeScript / JavaScript
|
||||||
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||||
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||||
|
|||||||
102
CHANGELOG.md
@@ -1,5 +1,107 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.13.5 (2024-06-26)
|
||||||
|
|
||||||
|
* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703)
|
||||||
|
* win: document and improve Firefox telemetry #259 | [8341411](https://github.com/undergroundwires/privacy.sexy/commit/8341411be434c6d145e942b1792020ccf02f58c8)
|
||||||
|
* Add image to `README.md` to thank supporters | [fa2a92b](https://github.com/undergroundwires/privacy.sexy/commit/fa2a92bf893933bf5cd04512a712b7aa1b921277)
|
||||||
|
* win: improve executable blocking, Chrome reporting | [f21ef92](https://github.com/undergroundwires/privacy.sexy/commit/f21ef9250a2f459dbd4f789d857c78298fc202e6)
|
||||||
|
* mac: discourage and document captive portal script | [b29cd7b](https://github.com/undergroundwires/privacy.sexy/commit/b29cd7b5f74accf92c9700c3171670f82c8cb3b3)
|
||||||
|
* win: fix revert scripts for removing shortcuts | [8becc7d](https://github.com/undergroundwires/privacy.sexy/commit/8becc7dbc46af4441900e9841a716a53735bc82e)
|
||||||
|
* Refactor to unify scripts/categories as Executable | [c138f74](https://github.com/undergroundwires/privacy.sexy/commit/c138f74460bafaba3da55a65f3942bb6f95b1d99)
|
||||||
|
* Add object property validation in parser #369 | [6ecfa9b](https://github.com/undergroundwires/privacy.sexy/commit/6ecfa9b954edc10401acaf5c735eec0fc9f991cd)
|
||||||
|
* win: fix missing app access recommendations #369 | [1c2d82d](https://github.com/undergroundwires/privacy.sexy/commit/1c2d82dc9bd412ea601ab2550ba0b4f7d144f8e8)
|
||||||
|
* win: fix text and handwriting script omission #369 | [1a10cf2](https://github.com/undergroundwires/privacy.sexy/commit/1a10cf2e5f87cd8eb421ef77f6ce764b5482515e)
|
||||||
|
* mac: document, improve, encourage clearing logs | [e9a5285](https://github.com/undergroundwires/privacy.sexy/commit/e9a52859f63609c3f56def0b3e4d1ac6e5661536)
|
||||||
|
* Add schema validation for collection files #369 | [dc03bff](https://github.com/undergroundwires/privacy.sexy/commit/dc03bff324d673101002bb16f14e0429e8170fbb)
|
||||||
|
* win: fix incomplete VSCEIP, location scripts | [48761f6](https://github.com/undergroundwires/privacy.sexy/commit/48761f62a242f0910307994271cbe6730fb30f7e)
|
||||||
|
* Add type validation for parameters and fix types | [fac26a6](https://github.com/undergroundwires/privacy.sexy/commit/fac26a6ca07479c84fe62c5ea2a572dad1898ef8)
|
||||||
|
* Bump Electron to latest | [ed93614](https://github.com/undergroundwires/privacy.sexy/commit/ed93614ca34b1ab166e645cc5bedd497b0caeaac)
|
||||||
|
* Trim compiler error output for better readability | [78c62cf](https://github.com/undergroundwires/privacy.sexy/commit/78c62cfc953dbba543d8bdc42828a4ef4b13a7c7)
|
||||||
|
* win: fix errors due to missing Edge uninstaller | [2f82873](https://github.com/undergroundwires/privacy.sexy/commit/2f828735a87f98ba87b4fc826823d1482d4f2db2)
|
||||||
|
* win: fix latest Edge removal on Windows 10 #309 | [e7031a3](https://github.com/undergroundwires/privacy.sexy/commit/e7031a3ae4e57b6522c6ca67fc30e8a8718506b2)
|
||||||
|
* win: categorize, rename, doc Chrome & Edge scripts | [f286f92](https://github.com/undergroundwires/privacy.sexy/commit/f286f92b1fec49e89eea8982dffbc3d6ef1defde)
|
||||||
|
* win: add disabling Edge/WebView2 auto-updates #309 | [ed7e69c](https://github.com/undergroundwires/privacy.sexy/commit/ed7e69c07efe83fdb7f4af13aa220ff991fbbe59)
|
||||||
|
* win, linux, mac: fix typos #373 | [c09c5ff](https://github.com/undergroundwires/privacy.sexy/commit/c09c5ffa47865f7c76910644558b6783ed44f1e4)
|
||||||
|
* win: add more Edge scripts including AI & ads | [1430d52](https://github.com/undergroundwires/privacy.sexy/commit/1430d5215ab094d8201710761d631dc2bd740918)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.4...0.13.5)
|
||||||
|
|
||||||
|
## 0.13.4 (2024-05-27)
|
||||||
|
|
||||||
|
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
|
||||||
|
* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda)
|
||||||
|
* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955)
|
||||||
|
* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2)
|
||||||
|
* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908)
|
||||||
|
* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f)
|
||||||
|
* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1)
|
||||||
|
* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb)
|
||||||
|
* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731)
|
||||||
|
* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8)
|
||||||
|
* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed)
|
||||||
|
* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74)
|
||||||
|
* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e)
|
||||||
|
* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29)
|
||||||
|
* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c)
|
||||||
|
* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e)
|
||||||
|
* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4)
|
||||||
|
|
||||||
|
## 0.13.3 (2024-05-11)
|
||||||
|
|
||||||
|
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)
|
||||||
|
* win: improve disabling SMBv1 protocol | [f584fab](https://github.com/undergroundwires/privacy.sexy/commit/f584fabb50c7de70ba43751d721af94d8fa2fa8a)
|
||||||
|
* win: improve disabling insecure renegotiations | [f261ab4](https://github.com/undergroundwires/privacy.sexy/commit/f261ab4cd9a53e31325e5c6da9129542971fe84b)
|
||||||
|
* win: doc, improve, encourage cipher disabling | [8b224ee](https://github.com/undergroundwires/privacy.sexy/commit/8b224eefe71be6a556a1085d8fe20dbd4b889430)
|
||||||
|
* ci/cd: add check for TODO comments | [4e21f05](https://github.com/undergroundwires/privacy.sexy/commit/4e21f05031d6cc90cda684bd598bec4735f8103b)
|
||||||
|
* win: improve 'Snipping Tool' removal #343 | [e18907c](https://github.com/undergroundwires/privacy.sexy/commit/e18907ca91e483255b44d14d7d923d7eef92afbd)
|
||||||
|
* ci/cd: lint Python scripts using `pylint` | [23bac0f](https://github.com/undergroundwires/privacy.sexy/commit/23bac0fc76ad697abb34f3fb327df5cdeb40286a)
|
||||||
|
* win: improve disabling insecure hashes #131 | [d19dde6](https://github.com/undergroundwires/privacy.sexy/commit/d19dde603ddac47022ee2e0ea865d53857560c26)
|
||||||
|
* Add system requirements documentation #134 | [0fc2ffc](https://github.com/undergroundwires/privacy.sexy/commit/0fc2ffc1ea36a9248c6a92da85a29f7b04b33796)
|
||||||
|
* win, linux, mac: fix various typos #349 | [694bf1a](https://github.com/undergroundwires/privacy.sexy/commit/694bf1a74d935531d7cd46891823af1fa58c3c8c)
|
||||||
|
* Fix script cancellation with new dialog on Linux | [8c17396](https://github.com/undergroundwires/privacy.sexy/commit/8c173962857a39dc0c9e5886cb2af4937e6618e7)
|
||||||
|
* win: improve disabling protocols | [4ef16ce](https://github.com/undergroundwires/privacy.sexy/commit/4ef16cea56789120cd041412d86b5577cccf0725)
|
||||||
|
* win: fix Copilot by excluding `r.bing.com` #329 | [66a5688](https://github.com/undergroundwires/privacy.sexy/commit/66a56888a4b3ead1a6bfef0feffa0218535701fe)
|
||||||
|
* Fix blank window on load on desktop version #348 | [813d820](https://github.com/undergroundwires/privacy.sexy/commit/813d820b85e1b623c50f8e0325ad372bf2f344f9)
|
||||||
|
* Improve desktop icon quality and generation | [ab25e0a](https://github.com/undergroundwires/privacy.sexy/commit/ab25e0a066be14ea979dafd0f80e1091bd5d33f8)
|
||||||
|
* win: improve enabling secure connections #175 | [c75df1c](https://github.com/undergroundwires/privacy.sexy/commit/c75df1c8c1151b64cbf014383dea0b748a8c78b3)
|
||||||
|
* Fix VSCode script issues with added CI/CD tests | [1d7cafc](https://github.com/undergroundwires/privacy.sexy/commit/1d7cafc831dcc339a10646794410dad7096bfe60)
|
||||||
|
* Fix win execution with whitespace in username #351 | [a334320](https://github.com/undergroundwires/privacy.sexy/commit/a3343205b1196d5a81fd3cee2ae661ce871a7bef)
|
||||||
|
* Fix misaligned tooltip positions in modal dialogs | [dd71536](https://github.com/undergroundwires/privacy.sexy/commit/dd71536316ec819caeb418b8635d544ac80e58ad)
|
||||||
|
* Fix Chromium scrollbar-induced layout shifts | [bc4879c](https://github.com/undergroundwires/privacy.sexy/commit/bc4879cfe97becac3c54f6b40780a89464d3b772)
|
||||||
|
* ci/cd: remove `check-latest` from `setup-node` | [52a4730](https://github.com/undergroundwires/privacy.sexy/commit/52a4730073b8ebfb2ce9d530b44e4a179f5849fe)
|
||||||
|
* win: categorize and rename network security #131 | [9fd193e](https://github.com/undergroundwires/privacy.sexy/commit/9fd193e676f1f0646898f5130fbfaaf25050b2e3)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.2...0.13.3)
|
||||||
|
|
||||||
|
## 0.13.2 (2024-04-15)
|
||||||
|
|
||||||
|
* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796)
|
||||||
|
* win: improve and document removing Phone apps #279 | [8924337](https://github.com/undergroundwires/privacy.sexy/commit/89243371faa5d6aef5fce52b0d54a442143cdd39)
|
||||||
|
* Fix bottom gap in card expansion panel | [79183d6](https://github.com/undergroundwires/privacy.sexy/commit/79183d64173e588d88bf074d5b50a52a71c2d885)
|
||||||
|
* ci/cd: Fix macOS Docker build reliability issues | [8a5592f](https://github.com/undergroundwires/privacy.sexy/commit/8a5592f92be4366a806afc9eee9135696a1dd993)
|
||||||
|
* ci/cd: fix IPv6 timeouts with `force-ipv4` action | [52fadcd](https://github.com/undergroundwires/privacy.sexy/commit/52fadcd6177ed06216be9c67dad57192ae02a4f9)
|
||||||
|
* ci/cd: bump Node.js environment to 20.x | [59decd1](https://github.com/undergroundwires/privacy.sexy/commit/59decd17e273bada1493eaa855c43cbabf90308f)
|
||||||
|
* ci/cd: trigger URL checks more, and limit amount | [4fb6302](https://github.com/undergroundwires/privacy.sexy/commit/4fb6302c67f2a3fedff419e8c22872593cf800ef)
|
||||||
|
* Fix overflow in tree node content on small screens | [557cea3](https://github.com/undergroundwires/privacy.sexy/commit/557cea3f4866dc33236874f5fe4d2d69ee963dae)
|
||||||
|
* Fix horizontal layout shift after script selection | [bc7e1fa](https://github.com/undergroundwires/privacy.sexy/commit/bc7e1faa1c3f2b61bf2046fdd6d6a4141b484662)
|
||||||
|
* Fix card header expansion glitch on card collapse | [5d940b5](https://github.com/undergroundwires/privacy.sexy/commit/5d940b57ef2a4c219932cd15201401f8550cfb41)
|
||||||
|
* Ignore `ResizeObserver` errors in Cypress tests | [4472c28](https://github.com/undergroundwires/privacy.sexy/commit/4472c2852e4b87083bda7979471ab9f377d17a01)
|
||||||
|
* win: improve and document secret key scripts | [49f22f0](https://github.com/undergroundwires/privacy.sexy/commit/49f22f048f39e7388633c488b5fe59101b831984)
|
||||||
|
* Fix card arrow not being animated in sync | [7b546c5](https://github.com/undergroundwires/privacy.sexy/commit/7b546c567c4683a37fe94595362f4c2bf92ffd59)
|
||||||
|
* win: improve Windows feature disablement scripts | [b68711e](https://github.com/undergroundwires/privacy.sexy/commit/b68711ef88982c0ee2b1d41b4452e899821adc64)
|
||||||
|
* Fix top script menu overflow on small screens | [b7a20d9](https://github.com/undergroundwires/privacy.sexy/commit/b7a20d9d41ea8bcefdd553b87641f3c22b4cde97)
|
||||||
|
* win: fix Visual Studio remote analysis script #327 | [4142d08](https://github.com/undergroundwires/privacy.sexy/commit/4142d084f64a3b540487ff68b28032977d12006d)
|
||||||
|
* win: improve firewall docs /w `winget` impact #142 | [ffd647d](https://github.com/undergroundwires/privacy.sexy/commit/ffd647d1529375474b81900cc7bee4c32fbf861f)
|
||||||
|
* Centralize and use global spacing variables | [ae17200](https://github.com/undergroundwires/privacy.sexy/commit/ae172000a64416e5a3e2b2e32b7846f039f445f0)
|
||||||
|
* win: improve service revert and docs | [b87b7aa](https://github.com/undergroundwires/privacy.sexy/commit/b87b7aac7d118a23a0d1bfb881e385347de4adb7)
|
||||||
|
* Bump dependencies to latest, hold ESLint | [f3571ab](https://github.com/undergroundwires/privacy.sexy/commit/f3571abeafdbe1e6d152958fab26de91a9c08bc3)
|
||||||
|
* Fix inability to tap outside modal on mobile | [cb144ae](https://github.com/undergroundwires/privacy.sexy/commit/cb144ae47273deeb7058d4b1380e480ebccdaf81)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.1...0.13.2)
|
||||||
|
|
||||||
## 0.13.1 (2024-03-22)
|
## 0.13.1 (2024-03-22)
|
||||||
|
|
||||||
* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010)
|
* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010)
|
||||||
|
|||||||
15
README.md
@@ -60,8 +60,8 @@
|
|||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
alt="Quality checks status"
|
alt="Status of quality checks"
|
||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.quality/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
@@ -122,9 +122,12 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-Setup-0.13.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-0.13.1.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-0.13.1.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-Setup-0.13.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
For a detailed comparison of features between the desktop and web versions of privacy.sexy, see [Desktop vs. Web Features](./docs/desktop-vs-web-features.md).
|
See also:
|
||||||
|
|
||||||
|
- [Desktop vs. Web Features](./docs/desktop/desktop-vs-web-features.md): Differences and unique aspects of desktop and web versions.
|
||||||
|
- [System Requirements](./docs/desktop/system-requirements.md): Hardware and software requirements for the desktop version.
|
||||||
|
|
||||||
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
||||||
|
|
||||||
@@ -183,3 +186,7 @@ Check [architecture.md](./docs/architecture.md) for an overview of design and ho
|
|||||||
Security is a top priority at privacy.sexy.
|
Security is a top priority at privacy.sexy.
|
||||||
An extensive commitment to security verification ensures this priority.
|
An extensive commitment to security verification ensures this priority.
|
||||||
For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
|
For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
|
||||||
|
|
||||||
|
## Supporters
|
||||||
|
|
||||||
|
[](https://undergroundwires.dev/supporters)
|
||||||
|
|||||||
15
SECURITY.md
@@ -43,10 +43,17 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
|
|||||||
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
||||||
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
||||||
- **Secure Script Execution/Storage:**
|
- **Secure Script Execution/Storage:**
|
||||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
|
- **Antivirus scans:**
|
||||||
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
|
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans.
|
||||||
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
|
This step allows confirming that the scripts are secure and safe to use.
|
||||||
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
- **Tamper protection:**
|
||||||
|
The application incorporates integrity checks for tamper protection.
|
||||||
|
If the script file differs from the user's selected script, the application will not execute or save the script, ensuring the processing
|
||||||
|
of authentic scripts.
|
||||||
|
This safeguards against any unwanted modifications.
|
||||||
|
- **Clean-up:**
|
||||||
|
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
||||||
|
This allows users to maintain their privacy by removing traces of their usage patterns or script preferences.
|
||||||
|
|
||||||
### Update Security and Integrity
|
### Update Security and Integrity
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# build
|
|
||||||
|
|
||||||
This folder contains files that are used by Electron to serve the desktop version.
|
|
||||||
|
|
||||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
|
||||||
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -41,5 +41,5 @@ Application layer compiles templating syntax during parsing to create the end sc
|
|||||||
|
|
||||||
The steps to extend the templating syntax:
|
The steps to extend the templating syntax:
|
||||||
|
|
||||||
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||||
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Collection files
|
# Collection files
|
||||||
|
|
||||||
privacy.sexy is a data-driven application that reads YAML files.
|
privacy.sexy is a data-driven application that reads YAML files.
|
||||||
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model.
|
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. The YAML schema [`.schema.yaml`](./../src/application/collections/.schema.yaml) is provided to provide better IDE support and be used in automated validations.
|
||||||
|
|
||||||
Related documentation:
|
Related documentation:
|
||||||
|
|
||||||
- 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types.
|
- 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation.
|
||||||
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
|
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
|
||||||
|
|
||||||
## Objects
|
## Objects
|
||||||
@@ -28,11 +28,22 @@ Related documentation:
|
|||||||
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||||
- Sets the scripting language for all inline code used within the collection.
|
- Sets the scripting language for all inline code used within the collection.
|
||||||
|
|
||||||
### `Category`
|
### Executables
|
||||||
|
|
||||||
|
They represent independently executable actions with documentation and reversibility.
|
||||||
|
|
||||||
|
An Executable is a logical entity that can
|
||||||
|
|
||||||
|
- execute once compiled,
|
||||||
|
- include a `docs` property for documentation.
|
||||||
|
|
||||||
|
It's either [Category](#category) or a [Script](#script).
|
||||||
|
|
||||||
|
#### `Category`
|
||||||
|
|
||||||
Represents a logical group of scripts and subcategories.
|
Represents a logical group of scripts and subcategories.
|
||||||
|
|
||||||
#### `Category` syntax
|
##### `Category` syntax
|
||||||
|
|
||||||
- `category:` *`string`* **(required)**
|
- `category:` *`string`* **(required)**
|
||||||
- Name of the category.
|
- Name of the category.
|
||||||
@@ -43,7 +54,7 @@ Represents a logical group of scripts and subcategories.
|
|||||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
- Markdown-formatted documentation related to the category.
|
- Markdown-formatted documentation related to the category.
|
||||||
|
|
||||||
### `Script`
|
#### `Script`
|
||||||
|
|
||||||
Represents an individual tweak.
|
Represents an individual tweak.
|
||||||
|
|
||||||
@@ -58,7 +69,7 @@ Types (like [functions](#function)):
|
|||||||
|
|
||||||
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
|
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
|
||||||
|
|
||||||
#### `Script` syntax
|
##### `Script` syntax
|
||||||
|
|
||||||
- `name`: *`string`* **(required)**
|
- `name`: *`string`* **(required)**
|
||||||
- Script name.
|
- Script name.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Desktop vs. Web Features
|
# Desktop vs. Web Features
|
||||||
|
|
||||||
This table highlights differences between the desktop and web versions of `privacy.sexy`.
|
This table outlines the differences between the desktop and web versions of `privacy.sexy`.
|
||||||
|
|
||||||
| Feature | Desktop | Web |
|
| Feature | Desktop | Web |
|
||||||
| ------- | ------- | --- |
|
| ------- | ------- | --- |
|
||||||
@@ -8,10 +8,8 @@ This table highlights differences between the desktop and web versions of `priva
|
|||||||
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
||||||
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
||||||
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
|
||||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
|
||||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
|
||||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||||
|
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||||
|
|
||||||
## Feature descriptions
|
## Feature descriptions
|
||||||
|
|
||||||
@@ -30,11 +28,11 @@ Desktop version inherently allows offline usage.
|
|||||||
|
|
||||||
### Auto-updates
|
### Auto-updates
|
||||||
|
|
||||||
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./ci-cd.md).
|
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./../ci-cd.md).
|
||||||
|
|
||||||
The desktop version ensures secure delivery through cryptographic signatures and version checks.
|
The desktop version ensures secure delivery through cryptographic signatures and version checks.
|
||||||
|
|
||||||
[Security is a top priority](./../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
[Security is a top priority](./../../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
||||||
|
|
||||||
> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
||||||
> Users get notified about updates but might need to complete the installation manually.
|
> Users get notified about updates but might need to complete the installation manually.
|
||||||
@@ -53,7 +51,7 @@ Log file locations vary by operating system:
|
|||||||
|
|
||||||
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
||||||
|
|
||||||
### Script execution
|
### Secure script execution/storage
|
||||||
|
|
||||||
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
||||||
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
||||||
@@ -69,31 +67,27 @@ These locations vary based on the operating system:
|
|||||||
|
|
||||||
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
||||||
|
|
||||||
### Error handling
|
**Script antivirus scans:**
|
||||||
|
|
||||||
The desktop version of privacy.sexy features advanced error handling capabilities.
|
To enhance system protection, the desktop version of privacy.sexy automatically verifies the security of script
|
||||||
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
execution files by reading them back.
|
||||||
In contrast, the web version has more basic error handling due to browser limitations and the nature of web applications.
|
This process triggers antivirus scans to verify that scripts are safe before the execution.
|
||||||
|
|
||||||
### Native dialogs
|
**Script integrity checks:**
|
||||||
|
|
||||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
|
||||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
|
||||||
|
|
||||||
### Secure script execution/storage
|
|
||||||
|
|
||||||
**Integrity checks:**
|
|
||||||
|
|
||||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||||
Due to browser constraints, this feature is absent in the web version.
|
|
||||||
|
|
||||||
**Error handling:**
|
**Error handling:**
|
||||||
|
|
||||||
|
The desktop version of privacy.sexy features advanced error handling capabilities.
|
||||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||||
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
|
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
||||||
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
|
This proactive error handling and user guidance enhances the application's security and reliability.
|
||||||
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
|
|
||||||
offering a feature not achievable in the web version due to browser limitations.
|
### Native dialogs
|
||||||
|
|
||||||
|
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||||
|
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||||
36
docs/desktop/system-requirements.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# System Requirements for the Desktop Version
|
||||||
|
|
||||||
|
The following system requirements are the official ones for the desktop version.
|
||||||
|
While we have tested and confirmed these requirements, the application might also work on other
|
||||||
|
systems or configurations that haven't undergone official testing.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
- **Version:** Windows 10 and later.
|
||||||
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
|
- **Architecture:** 64-bit (x86-64), ARM (ARM64).
|
||||||
|
|
||||||
|
> **⚠️ Compatibility Note:**
|
||||||
|
> ARM version is only compatible with Windows 11 and later.
|
||||||
|
> It runs non-natively, leading to slower performance due to emulation [1].
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
- **Version:** macOS Catalina (10.15) and later.
|
||||||
|
- **Architecture:** Intel-based (x86-64), Apple silicon (ARM64).
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later.
|
||||||
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
|
- **Architecture:** 64-bit (x86-64).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
System requirements reflect Electron's platform capabilities [2] and Chromium's recommended configurations [3].
|
||||||
|
|
||||||
|
For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs).
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com"
|
||||||
|
[2]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com"
|
||||||
|
[3]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com"
|
||||||
@@ -80,8 +80,10 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
|||||||
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
||||||
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||||
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
||||||
- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
|
- [**`python3 ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
|
||||||
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
|
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
|
||||||
|
- [**`python3 ./scripts/validate-collections-yaml`**](../scripts/validate-collections-yaml/README.md):
|
||||||
|
- Validates the syntax and structure of collection YAML files.
|
||||||
|
|
||||||
#### Automation scripts
|
#### Automation scripts
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
- [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
- [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
|
- [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes.
|
||||||
|
- [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md).
|
||||||
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||||
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Key attributes of a good script:
|
|||||||
- `Minimize` over `Limit`, `Reduce`
|
- `Minimize` over `Limit`, `Reduce`
|
||||||
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
||||||
- `Remove` over `Uninstall`
|
- `Remove` over `Uninstall`
|
||||||
|
- `Improve` over `Increase`
|
||||||
- Structure your phrases for clarity, examples:
|
- Structure your phrases for clarity, examples:
|
||||||
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
||||||
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable no-template-curly-in-string */
|
/* eslint-disable no-template-curly-in-string */
|
||||||
|
|
||||||
const { join } = require('node:path');
|
const { join, resolve } = require('node:path');
|
||||||
const { readdirSync } = require('fs');
|
const { readdirSync, existsSync } = require('node:fs');
|
||||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +17,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
directories: {
|
directories: {
|
||||||
output: electronBundled,
|
output: electronBundled,
|
||||||
|
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
|
||||||
},
|
},
|
||||||
extraMetadata: {
|
extraMetadata: {
|
||||||
main: findMainEntryFile(
|
main: findMainEntryFile(
|
||||||
@@ -42,7 +43,10 @@ module.exports = {
|
|||||||
|
|
||||||
// macOS
|
// macOS
|
||||||
mac: {
|
mac: {
|
||||||
target: 'dmg',
|
target: {
|
||||||
|
target: 'dmg',
|
||||||
|
arch: 'universal',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dmg: {
|
dmg: {
|
||||||
artifactName: '${name}-${version}.${ext}',
|
artifactName: '${name}-${version}.${ext}',
|
||||||
@@ -53,10 +57,18 @@ module.exports = {
|
|||||||
* Finds by accommodating different JS file extensions and module formats.
|
* Finds by accommodating different JS file extensions and module formats.
|
||||||
*/
|
*/
|
||||||
function findMainEntryFile(parentDirectory) {
|
function findMainEntryFile(parentDirectory) {
|
||||||
const files = readdirSync(parentDirectory);
|
const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory);
|
||||||
|
if (!existsSync(absoluteParentDirectory)) {
|
||||||
|
return null; // Avoid disrupting other processes such `npm install`.
|
||||||
|
}
|
||||||
|
const files = readdirSync(absoluteParentDirectory);
|
||||||
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
||||||
if (!entryFile) {
|
if (!entryFile) {
|
||||||
throw new Error(`Main entry file not found in ${parentDirectory}.`);
|
throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
|
||||||
}
|
}
|
||||||
return join(parentDirectory, entryFile);
|
return join(parentDirectory, entryFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePathFromProjectRoot(pathSegment) {
|
||||||
|
return resolve(__dirname, pathSegment);
|
||||||
|
}
|
||||||
|
|||||||
9361
package-lock.json
generated
62
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.1",
|
"version": "0.13.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Privacy is sexy",
|
"slogan": "Privacy is sexy",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"test:integration": "vitest run --dir tests/integration",
|
"test:integration": "vitest run --dir tests/integration",
|
||||||
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||||
"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 && npm run lint:pylint",
|
||||||
"install-deps": "node scripts/npm-install.js",
|
"install-deps": "node scripts/npm-install.js",
|
||||||
"icons:build": "node scripts/logo-update.js",
|
"icons:build": "node scripts/logo-update.js",
|
||||||
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||||
@@ -29,61 +29,59 @@
|
|||||||
"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:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
|
"lint:pylint": "pylint **/*.py",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.0.6",
|
"@floating-ui/vue": "^1.1.1",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.33.0",
|
"ace-builds": "^1.35.3",
|
||||||
"electron-log": "^5.1.2",
|
"electron-log": "^5.1.6",
|
||||||
"electron-progressbar": "^2.2.1",
|
"electron-progressbar": "^2.2.1",
|
||||||
"electron-updater": "^6.1.9",
|
"electron-updater": "^6.2.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"vue": "^3.4.21"
|
"vue": "^3.4.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
"@rushstack/eslint-patch": "^1.10.2",
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
"@types/ace": "^0.0.52",
|
"@types/ace": "^0.0.52",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/markdown-it": "^14.0.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "6.21.0",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"@vitejs/plugin-legacy": "^5.3.2",
|
"@vitejs/plugin-legacy": "^5.4.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
||||||
"@vue/eslint-config-typescript": "12.0.0",
|
"@vue/eslint-config-typescript": "12.0.0",
|
||||||
"@vue/test-utils": "^2.4.5",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"cypress": "^13.7.3",
|
"cypress": "^13.13.1",
|
||||||
"electron": "^29.3.0",
|
"electron": "^31.2.1",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-vite": "^2.3.0",
|
||||||
"electron-vite": "^2.1.0",
|
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^3.3.0",
|
||||||
"eslint-plugin-vue": "^9.25.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
"eslint-plugin-vuejs-accessibility": "^2.4.0",
|
||||||
"icon-gen": "^4.0.0",
|
"jsdom": "^24.1.0",
|
||||||
"jsdom": "^24.0.0",
|
"markdownlint-cli": "^0.41.0",
|
||||||
"markdownlint-cli": "^0.39.0",
|
"postcss": "^8.4.39",
|
||||||
"postcss": "^8.4.38",
|
"remark-cli": "^12.0.1",
|
||||||
"remark-cli": "^12.0.0",
|
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^6.0.0",
|
"remark-preset-lint-consistent": "^6.0.0",
|
||||||
"remark-validate-links": "^13.0.1",
|
"remark-validate-links": "^13.0.1",
|
||||||
"sass": "^1.75.0",
|
"sass": "^1.77.8",
|
||||||
"start-server-and-test": "^2.0.3",
|
"start-server-and-test": "^2.0.4",
|
||||||
"svgexport": "^0.4.2",
|
"terser": "^5.31.3",
|
||||||
"terser": "^5.30.3",
|
"tslib": "^2.6.3",
|
||||||
"tslib": "^2.6.2",
|
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.3.4",
|
||||||
"vitest": "^1.5.0",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^2.0.13",
|
"vue-tsc": "^2.0.26",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
Description:
|
||||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||||
|
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 ./scripts/configure_vscode.py
|
||||||
"""
|
"""
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ def ensure_setting_file_exists() -> None:
|
|||||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||||
except IOError as error:
|
except IOError as error:
|
||||||
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
||||||
print(f"📄 Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||||
|
|
||||||
def add_or_update_settings() -> None:
|
def add_or_update_settings() -> None:
|
||||||
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
||||||
@@ -54,6 +58,10 @@ def add_or_update_settings() -> None:
|
|||||||
# Details: # pylint: disable-next=line-too-long
|
# Details: # pylint: disable-next=line-too-long
|
||||||
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
|
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
|
||||||
|
|
||||||
|
# Disable telemetry
|
||||||
|
configure_setting_key('redhat.telemetry.enabled', False)
|
||||||
|
configure_setting_key('gitlens.telemetry.enabled', False)
|
||||||
|
|
||||||
def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
|
def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
|
||||||
try:
|
try:
|
||||||
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
|
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
|
||||||
@@ -98,7 +106,8 @@ def locate_vscode_cli() -> Optional[str]:
|
|||||||
if vscode_alias:
|
if vscode_alias:
|
||||||
return vscode_alias
|
return vscode_alias
|
||||||
potential_vscode_cli_paths = [
|
potential_vscode_cli_paths = [
|
||||||
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' # macOS VS Code may not register 'code' command in PATH
|
# VS Code on macOS may not register 'code' command in PATH
|
||||||
|
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'
|
||||||
]
|
]
|
||||||
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
||||||
if Path(vscode_cli_candidate_path).is_file():
|
if Path(vscode_cli_candidate_path).is_file():
|
||||||
@@ -109,7 +118,7 @@ def remove_json_comments(json_like: str) -> str:
|
|||||||
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
||||||
return re.sub(
|
return re.sub(
|
||||||
pattern,
|
pattern,
|
||||||
lambda m: '' if m.group(1) else m.agroup(0), json_like, flags=re.MULTILINE,
|
lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
||||||
@@ -166,16 +175,16 @@ def print_installation_results(successful_installations: int, total_extensions:
|
|||||||
print_error("Failed to install any of the recommended extensions.")
|
print_error("Failed to install any of the recommended extensions.")
|
||||||
|
|
||||||
def print_error(message: str) -> None:
|
def print_error(message: str) -> None:
|
||||||
print(f"💀 Error: {message}", file=sys.stderr)
|
print(f"[ERROR] {message}", file=sys.stderr)
|
||||||
|
|
||||||
def print_success(message: str) -> None:
|
def print_success(message: str) -> None:
|
||||||
print(f"✅ Success: {message}")
|
print(f"[SUCCESS] {message}")
|
||||||
|
|
||||||
def print_skip(message: str) -> None:
|
def print_skip(message: str) -> None:
|
||||||
print(f"⏩ Skipped: {message}")
|
print(f"[SKIPPED] {message}")
|
||||||
|
|
||||||
def print_warning(message: str) -> None:
|
def print_warning(message: str) -> None:
|
||||||
print(f"⚠️ Warning: {message}", file=sys.stderr)
|
print(f"[WARNING] {message}", file=sys.stderr)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -2,94 +2,119 @@
|
|||||||
* Description:
|
* Description:
|
||||||
* This script updates the logo images across the project based on the primary
|
* This script updates the logo images across the project based on the primary
|
||||||
* logo file ('img/logo.svg' file).
|
* logo file ('img/logo.svg' file).
|
||||||
|
*
|
||||||
* It handles the creation and update of various icon sizes for different purposes,
|
* It handles the creation and update of various icon sizes for different purposes,
|
||||||
* including desktop launcher icons, tray icons, and web favicons from a single source
|
* including desktop launcher icons, tray icons, and web favicons from a single source
|
||||||
* SVG logo file.
|
* SVG logo file.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node ./scripts/logo-update.js
|
* node ./scripts/logo-update.js
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* ImageMagick must be installed and accessible in the system's PATH
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { resolve, join } from 'node:path';
|
import { resolve, join, dirname } from 'node:path';
|
||||||
import { rm, mkdtemp, stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { URL, fileURLToPath } from 'node:url';
|
import { URL, fileURLToPath } from 'node:url';
|
||||||
|
import electronBuilderConfig from '../electron-builder.cjs';
|
||||||
|
|
||||||
class Paths {
|
class ImageAssetPaths {
|
||||||
constructor(selfDirectory) {
|
constructor(currentScriptDirectory) {
|
||||||
const projectRoot = resolve(selfDirectory, '../');
|
const projectRoot = resolve(currentScriptDirectory, '../');
|
||||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
get electronTrayIconFile() {
|
||||||
|
return join(this.publicDirectory, 'icon.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
get webFaviconFile() {
|
||||||
|
return join(this.publicDirectory, 'favicon.ico');
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `Source image: ${this.sourceImage}\n`
|
return `Source image: ${this.sourceImage}`
|
||||||
+ `Public directory: ${this.publicDirectory}\n`
|
+ `\nPublic directory: ${this.publicDirectory}`
|
||||||
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
+ `\n\t Electron tray icon file: ${this.electronTrayIconFile}`
|
||||||
|
+ `\n\t Web favicon file: ${this.webFaviconFile}`
|
||||||
|
+ `\nElectron build directory: ${this.electronBuildResourcesDirectory}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const paths = new Paths(getCurrentScriptDirectory());
|
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
|
||||||
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||||
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
const convertCommand = await findAvailableImageMagickCommand();
|
||||||
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
await generateDesktopAndTrayIcons(
|
||||||
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
paths.sourceImage,
|
||||||
|
paths.electronTrayIconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateWebFavicon(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.webFaviconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateDesktopIcons(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.electronBuildResourcesDirectory,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
console.log('🎉 (Re)created icons successfully.');
|
console.log('🎉 (Re)created icons successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) {
|
||||||
|
// Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray
|
||||||
|
console.log(`Updating desktop launcher and tray icon at ${targetFile}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(publicFolder);
|
await ensureParentFolderExists(targetFile);
|
||||||
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
await convertFromSvgToPng(
|
||||||
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
convertCommand,
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
electronTrayIconFile,
|
targetFile,
|
||||||
|
'512x512',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateWebFavicon(sourceImage, faviconFolder) {
|
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
|
||||||
console.log('Updating favicon');
|
console.log(`Updating favicon at ${faviconFilePath}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(faviconFolder);
|
await ensureParentFolderExists(faviconFilePath);
|
||||||
await runCommand(
|
await convertFromSvgToIco(
|
||||||
'npx',
|
convertCommand,
|
||||||
'icon-gen',
|
sourceImage,
|
||||||
`--input ${sourceImage}`,
|
faviconFilePath,
|
||||||
`--output ${faviconFolder}`,
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
'--ico',
|
|
||||||
'--ico-name \'favicon\'',
|
|
||||||
'--report',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) {
|
||||||
|
console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`);
|
||||||
|
// Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html
|
||||||
|
await ensureFolderExists(electronBuildResourcesDirectory);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(electronIconsDir);
|
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
|
||||||
const temporaryDir = await mkdtemp('icon-');
|
await convertFromSvgToPng(
|
||||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
convertCommand,
|
||||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
temporaryPngFile,
|
electronMainIconFile,
|
||||||
'1024:1024',
|
'1024x1024', // Should be at least 512x512
|
||||||
);
|
);
|
||||||
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
// Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows
|
||||||
await runCommand(
|
// 10 and 11 according to tests, see:
|
||||||
'npx',
|
// - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328
|
||||||
'electron-icon-builder',
|
// - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867
|
||||||
`--input="${temporaryPngFile}"`,
|
const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico');
|
||||||
`--output="${electronIconsDir}"`,
|
await convertFromSvgToIco(
|
||||||
'--flatten',
|
convertCommand,
|
||||||
|
sourceImage,
|
||||||
|
electronWindowsIconFile,
|
||||||
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
);
|
);
|
||||||
console.log('Cleaning up temporary directory.');
|
|
||||||
await rm(temporaryDir, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFileExists(filePath) {
|
async function ensureFileExists(filePath) {
|
||||||
@@ -100,12 +125,60 @@ async function ensureFileExists(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFolderExists(folderPath) {
|
async function ensureFolderExists(folderPath) {
|
||||||
|
if (!folderPath) {
|
||||||
|
throw new Error('Path is missing');
|
||||||
|
}
|
||||||
const path = await stat(folderPath);
|
const path = await stat(folderPath);
|
||||||
if (!path.isDirectory()) {
|
if (!path.isDirectory()) {
|
||||||
throw new Error(`Not a directory: ${folderPath}`);
|
throw new Error(`Not a directory: ${folderPath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureParentFolderExists(filePath) {
|
||||||
|
return ensureFolderExists(dirname(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseImageMagickConvertArguments = Object.freeze([
|
||||||
|
'-background none', // Transparent, so they do not get filled with white.
|
||||||
|
'-strip', // Strip metadata.
|
||||||
|
'-gravity Center', // Center the image when there's empty space
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function convertFromSvgToIco(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
sizes,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
`-density ${Math.max(...sizes).toString()}`, // High enough for sharpness
|
||||||
|
`-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image
|
||||||
|
'-compress None',
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertFromSvgToPng(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
size = undefined,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
...(size === undefined ? [] : [
|
||||||
|
`-resize ${size}`,
|
||||||
|
`-density ${size}`, // High enough for sharpness
|
||||||
|
]),
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function runCommand(...args) {
|
async function runCommand(...args) {
|
||||||
const command = args.join(' ');
|
const command = args.join(' ');
|
||||||
console.log(`Running command: ${command}`);
|
console.log(`Running command: ${command}`);
|
||||||
@@ -135,4 +208,27 @@ function getCurrentScriptDirectory() {
|
|||||||
return fileURLToPath(new URL('.', import.meta.url));
|
return fileURLToPath(new URL('.', import.meta.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findAvailableImageMagickCommand() {
|
||||||
|
// Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php
|
||||||
|
const potentialBaseCommands = [
|
||||||
|
'convert', // Legacy command, usually available on Linux/macOS installations
|
||||||
|
'magick convert', // Newer command, available on Windows installations
|
||||||
|
];
|
||||||
|
for (const baseCommand of potentialBaseCommands) {
|
||||||
|
const testCommand = `${baseCommand} -version`;
|
||||||
|
try {
|
||||||
|
await runCommand(testCommand); // eslint-disable-line no-await-in-loop
|
||||||
|
console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`);
|
||||||
|
return baseCommand;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error([
|
||||||
|
'Unable to locate any operational ImageMagick command.',
|
||||||
|
`Attempted commands were: ${potentialBaseCommands.join(', ')}.`,
|
||||||
|
'Please ensure ImageMagick is correctly installed and accessible.',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|||||||
51
scripts/validate-collections-yaml/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# validate-collections-yaml
|
||||||
|
|
||||||
|
This script validates YAML collection files against a predefined schema to ensure their integrity.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python 3.x installed on your system.
|
||||||
|
|
||||||
|
## Running in a Virtual Environment (Recommended)
|
||||||
|
|
||||||
|
Using a virtual environment isolates dependencies and prevents conflicts.
|
||||||
|
|
||||||
|
1. **Create a virtual environment:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv ./scripts/validate-collections-yaml/.venv
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Activate the virtual environment:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ./scripts/validate-collections-yaml/.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install dependencies:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the script:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./scripts/validate-collections-yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Globally
|
||||||
|
|
||||||
|
Running the script globally is less recommended due to potential dependency conflicts.
|
||||||
|
|
||||||
|
1. **Install dependencies:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the script:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./scripts/validate-collections-yaml
|
||||||
|
```
|
||||||
62
scripts/validate-collections-yaml/__main__.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Description:
|
||||||
|
This script validates collection YAML files against the expected schema.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 ./scripts/validate-collections-yaml
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This script requires the `jsonschema` and `pyyaml` packages (see requirements.txt).
|
||||||
|
"""
|
||||||
|
# pylint: disable=missing-function-docstring
|
||||||
|
from os import path
|
||||||
|
import sys
|
||||||
|
from glob import glob
|
||||||
|
from typing import List
|
||||||
|
from jsonschema import exceptions, validate # pylint: disable=import-error
|
||||||
|
import yaml # pylint: disable=import-error
|
||||||
|
|
||||||
|
SCHEMA_FILE_PATH = './src/application/collections/.schema.yaml'
|
||||||
|
COLLECTIONS_GLOB_PATTERN = './src/application/collections/*.yaml'
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
schema_yaml = read_file(SCHEMA_FILE_PATH)
|
||||||
|
schema_json = convert_yaml_to_json(schema_yaml)
|
||||||
|
collection_file_paths = find_collection_files(COLLECTIONS_GLOB_PATTERN)
|
||||||
|
print(f'Found {len(collection_file_paths)} YAML files to validate.')
|
||||||
|
|
||||||
|
total_invalid_files = 0
|
||||||
|
for collection_file_path in collection_file_paths:
|
||||||
|
file_name = path.basename(collection_file_path)
|
||||||
|
print(f'Validating {file_name}...')
|
||||||
|
collection_yaml = read_file(collection_file_path)
|
||||||
|
collection_json = convert_yaml_to_json(collection_yaml)
|
||||||
|
try:
|
||||||
|
validate(instance=collection_json, schema=schema_json)
|
||||||
|
print(f'Success: {file_name} is valid.')
|
||||||
|
except exceptions.ValidationError as err:
|
||||||
|
print(f'Error: Validation failed for {file_name}.', file=sys.stderr)
|
||||||
|
print(str(err), file=sys.stderr)
|
||||||
|
total_invalid_files += 1
|
||||||
|
|
||||||
|
if total_invalid_files > 0:
|
||||||
|
print(f'Validation complete with {total_invalid_files} invalid files.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print('Validation complete. All files are valid.')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def read_file(file_path: str) -> str:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
def find_collection_files(glob_pattern: str) -> List[str]:
|
||||||
|
files = glob(glob_pattern)
|
||||||
|
filtered_files = [f for f in files if not path.basename(f).startswith('.')]
|
||||||
|
return filtered_files
|
||||||
|
|
||||||
|
def convert_yaml_to_json(yaml_content: str) -> dict:
|
||||||
|
return yaml.safe_load(yaml_content)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
6
scripts/validate-collections-yaml/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
attrs==23.2.0
|
||||||
|
jsonschema==4.22.0
|
||||||
|
jsonschema-specifications==2023.12.1
|
||||||
|
PyYAML==6.0.1
|
||||||
|
referencing==0.35.1
|
||||||
|
rpds-py==0.18.1
|
||||||
@@ -91,7 +91,7 @@ async function verifyFilesExist(directoryPath, filePatterns) {
|
|||||||
if (!match) {
|
if (!match) {
|
||||||
die(
|
die(
|
||||||
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
||||||
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
|
`\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ export type CodeRunErrorType =
|
|||||||
| 'FileWriteError'
|
| 'FileWriteError'
|
||||||
| 'FileReadbackVerificationError'
|
| 'FileReadbackVerificationError'
|
||||||
| 'FilePathGenerationError'
|
| 'FilePathGenerationError'
|
||||||
| 'UnsupportedOperatingSystem'
|
| 'UnsupportedPlatform'
|
||||||
| 'FileExecutionError'
|
|
||||||
| 'DirectoryCreationError'
|
| 'DirectoryCreationError'
|
||||||
| 'UnexpectedError';
|
| 'FilePermissionChangeError'
|
||||||
|
| 'FileExecutionError'
|
||||||
|
| 'ExternalProcessTermination';
|
||||||
|
|
||||||
interface CodeRunStatus {
|
interface CodeRunStatus {
|
||||||
readonly success: boolean;
|
readonly success: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isFunction } from '@/TypeHelpers';
|
import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Provides a unified and resilient way to extend errors across platforms.
|
Provides a unified and resilient way to extend errors across platforms.
|
||||||
@@ -12,8 +12,8 @@ import { isFunction } from '@/TypeHelpers';
|
|||||||
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||||
*/
|
*/
|
||||||
export abstract class CustomError extends Error {
|
export abstract class CustomError extends Error {
|
||||||
constructor(message?: string, options?: ErrorOptions) {
|
constructor(...args: ConstructorArguments<typeof Error>) {
|
||||||
super(message, options);
|
super(...args);
|
||||||
|
|
||||||
fixPrototype(this, new.target.prototype);
|
fixPrototype(this, new.target.prototype);
|
||||||
ensureStackTrace(this);
|
ensureStackTrace(this);
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ export type EnumType = number | string;
|
|||||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||||
= { [key in T]: TEnumValue };
|
= { [key in T]: TEnumValue };
|
||||||
|
|
||||||
export interface IEnumParser<TEnum> {
|
export interface EnumParser<TEnum> {
|
||||||
parseEnum(value: string, propertyName: string): TEnum;
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
): IEnumParser<TEnumValue> {
|
): EnumParser<TEnumValue> {
|
||||||
return {
|
return {
|
||||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
};
|
};
|
||||||
|
|||||||
25
src/application/Common/Text/FilterEmptyStrings.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { isArray } from '@/TypeHelpers';
|
||||||
|
|
||||||
|
export type OptionalString = string | undefined | null;
|
||||||
|
|
||||||
|
export function filterEmptyStrings(
|
||||||
|
texts: readonly OptionalString[],
|
||||||
|
isArrayType: typeof isArray = isArray,
|
||||||
|
): string[] {
|
||||||
|
if (!isArrayType(texts)) {
|
||||||
|
throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`);
|
||||||
|
}
|
||||||
|
assertArrayItemsAreStringLike(texts);
|
||||||
|
return texts
|
||||||
|
.filter((title): title is string => Boolean(title));
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertArrayItemsAreStringLike(
|
||||||
|
texts: readonly unknown[],
|
||||||
|
): asserts texts is readonly OptionalString[] {
|
||||||
|
const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null));
|
||||||
|
if (invalidItems.length > 0) {
|
||||||
|
const invalidTypes = invalidItems.map((item) => typeof item).join(', ');
|
||||||
|
throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/application/Common/Text/IndentText.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { isString } from '@/TypeHelpers';
|
||||||
|
import { splitTextIntoLines } from './SplitTextIntoLines';
|
||||||
|
|
||||||
|
export function indentText(
|
||||||
|
text: string,
|
||||||
|
indentLevel = 1,
|
||||||
|
utilities: TextIndentationUtilities = DefaultUtilities,
|
||||||
|
): string {
|
||||||
|
if (!utilities.isStringType(text)) {
|
||||||
|
throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`);
|
||||||
|
}
|
||||||
|
if (indentLevel <= 0) {
|
||||||
|
throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`);
|
||||||
|
}
|
||||||
|
const indentation = '\t'.repeat(indentLevel);
|
||||||
|
return utilities.splitIntoLines(text)
|
||||||
|
.map((line) => (line ? `${indentation}${line}` : line))
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextIndentationUtilities {
|
||||||
|
readonly splitIntoLines: typeof splitTextIntoLines;
|
||||||
|
readonly isStringType: typeof isString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultUtilities: TextIndentationUtilities = {
|
||||||
|
splitIntoLines: splitTextIntoLines,
|
||||||
|
isStringType: isString,
|
||||||
|
};
|
||||||
11
src/application/Common/Text/SplitTextIntoLines.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { isString } from '@/TypeHelpers';
|
||||||
|
|
||||||
|
export function splitTextIntoLines(
|
||||||
|
text: string,
|
||||||
|
isStringType = isString,
|
||||||
|
): string[] {
|
||||||
|
if (!isStringType(text)) {
|
||||||
|
throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`);
|
||||||
|
}
|
||||||
|
return text.split(/\r\n|\r|\n/);
|
||||||
|
}
|
||||||
@@ -1,44 +1,164 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { PlatformTimer } from './PlatformTimer';
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
import type { Timer, TimeoutType } from './Timer';
|
import type { Timer, TimeoutType } from './Timer';
|
||||||
|
|
||||||
export type CallbackType = (..._: readonly unknown[]) => void;
|
export type CallbackType = (..._: readonly unknown[]) => void;
|
||||||
|
|
||||||
export function throttle(
|
export interface ThrottleOptions {
|
||||||
callback: CallbackType,
|
/** Skip the immediate execution of the callback on the first invoke */
|
||||||
waitInMs: number,
|
readonly excludeLeadingCall: boolean;
|
||||||
timer: Timer = PlatformTimer,
|
readonly timer: Timer;
|
||||||
): CallbackType {
|
|
||||||
const throttler = new Throttler(timer, waitInMs, callback);
|
|
||||||
return (...args: unknown[]) => throttler.invoke(...args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Throttler {
|
const DefaultOptions: ThrottleOptions = {
|
||||||
private queuedExecutionId: TimeoutType | undefined;
|
excludeLeadingCall: false,
|
||||||
|
timer: PlatformTimer,
|
||||||
|
};
|
||||||
|
|
||||||
private previouslyRun: number;
|
export interface ThrottleFunction {
|
||||||
|
(
|
||||||
|
callback: CallbackType,
|
||||||
|
waitInMs: number,
|
||||||
|
options?: Partial<ThrottleOptions>,
|
||||||
|
): CallbackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const throttle: ThrottleFunction = (
|
||||||
|
callback: CallbackType,
|
||||||
|
waitInMs: number,
|
||||||
|
options: Partial<ThrottleOptions> = DefaultOptions,
|
||||||
|
): CallbackType => {
|
||||||
|
const defaultedOptions: ThrottleOptions = {
|
||||||
|
...DefaultOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
|
||||||
|
return (...args: unknown[]) => throttler.invoke(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Throttler {
|
||||||
|
private lastExecutionTime: number | null = null;
|
||||||
|
|
||||||
|
private executionScheduler: DelayedCallbackScheduler;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timer: Timer,
|
|
||||||
private readonly waitInMs: number,
|
private readonly waitInMs: number,
|
||||||
private readonly callback: CallbackType,
|
private readonly callback: CallbackType,
|
||||||
|
private readonly options: ThrottleOptions,
|
||||||
) {
|
) {
|
||||||
if (!waitInMs) { throw new Error('missing delay'); }
|
if (!waitInMs) { throw new Error('missing delay'); }
|
||||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||||
|
this.executionScheduler = new DelayedCallbackScheduler(options.timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public invoke(...args: unknown[]): void {
|
public invoke(...args: unknown[]): void {
|
||||||
const now = this.timer.dateNow();
|
switch (true) {
|
||||||
if (this.queuedExecutionId !== undefined) {
|
case this.isLeadingCallWithinThrottlePeriod(): {
|
||||||
this.timer.clearTimeout(this.queuedExecutionId);
|
if (this.options.excludeLeadingCall) {
|
||||||
this.queuedExecutionId = undefined;
|
this.scheduleNext(args);
|
||||||
}
|
return;
|
||||||
if (!this.previouslyRun || (now - this.previouslyRun >= this.waitInMs)) {
|
}
|
||||||
this.callback(...args);
|
this.executeNow(args);
|
||||||
this.previouslyRun = now;
|
return;
|
||||||
} else {
|
}
|
||||||
const nextCall = () => this.invoke(...args);
|
case this.isAlreadyScheduled(): {
|
||||||
const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun);
|
this.updateNextScheduled(args);
|
||||||
this.queuedExecutionId = this.timer.setTimeout(nextCall, nextCallDelayInMs);
|
return;
|
||||||
|
}
|
||||||
|
case !this.isThrottlePeriodPassed(): {
|
||||||
|
this.scheduleNext(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Throttle logical error: no conditions for execution or scheduling were met.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isLeadingCallWithinThrottlePeriod(): boolean {
|
||||||
|
return this.isThrottlePeriodPassed()
|
||||||
|
&& !this.isAlreadyScheduled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isThrottlePeriodPassed(): boolean {
|
||||||
|
if (this.lastExecutionTime === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const timeSinceLastExecution = this.options.timer.dateNow() - this.lastExecutionTime;
|
||||||
|
const isThrottleTimePassed = timeSinceLastExecution >= this.waitInMs;
|
||||||
|
return isThrottleTimePassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAlreadyScheduled(): boolean {
|
||||||
|
return this.executionScheduler.getNext() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleNext(args: unknown[]): void {
|
||||||
|
if (this.executionScheduler.getNext()) {
|
||||||
|
throw new Error('An execution is already scheduled.');
|
||||||
|
}
|
||||||
|
this.executionScheduler.resetNext(
|
||||||
|
() => this.executeNow(args),
|
||||||
|
this.waitInMs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateNextScheduled(args: unknown[]): void {
|
||||||
|
const nextScheduled = this.executionScheduler.getNext();
|
||||||
|
if (!nextScheduled) {
|
||||||
|
throw new Error('A non-existent scheduled execution cannot be updated.');
|
||||||
|
}
|
||||||
|
const nextDelay = nextScheduled.scheduledTime - this.dateNow();
|
||||||
|
this.executionScheduler.resetNext(
|
||||||
|
() => this.executeNow(args),
|
||||||
|
nextDelay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeNow(args: unknown[]): void {
|
||||||
|
this.callback(...args);
|
||||||
|
this.lastExecutionTime = this.dateNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private dateNow(): number {
|
||||||
|
return this.options.timer.dateNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduledCallback {
|
||||||
|
readonly scheduleTimeoutId: TimeoutType;
|
||||||
|
readonly scheduledTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DelayedCallbackScheduler {
|
||||||
|
private scheduledCallback: ScheduledCallback | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly timer: Timer,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public getNext(): ScheduledCallback | null {
|
||||||
|
return this.scheduledCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetNext(
|
||||||
|
callback: () => void,
|
||||||
|
delayInMs: number,
|
||||||
|
) {
|
||||||
|
this.clear();
|
||||||
|
this.scheduledCallback = {
|
||||||
|
scheduledTime: this.timer.dateNow() + delayInMs,
|
||||||
|
scheduleTimeoutId: this.timer.setTimeout(() => {
|
||||||
|
this.clear();
|
||||||
|
callback();
|
||||||
|
}, delayInMs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private clear() {
|
||||||
|
if (this.scheduledCallback === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.timer.clearTimeout(this.scheduledCallback.scheduleTimeoutId);
|
||||||
|
this.scheduledCallback = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import type { IScript } from '@/domain/IScript';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
|
|
||||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
public readonly addedScripts: ReadonlyArray<Script>;
|
||||||
|
|
||||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
public readonly removedScripts: ReadonlyArray<Script>;
|
||||||
|
|
||||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
public readonly changedScripts: ReadonlyArray<Script>;
|
||||||
|
|
||||||
private readonly scripts: Map<IScript, ICodePosition>;
|
private readonly scripts: Map<Script, ICodePosition>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
code: string,
|
code: string,
|
||||||
@@ -25,7 +27,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||||
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||||
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||||
this.scripts = new Map<IScript, ICodePosition>();
|
this.scripts = new Map<Script, ICodePosition>();
|
||||||
scripts.forEach((position, selection) => {
|
scripts.forEach((position, selection) => {
|
||||||
this.scripts.set(selection.script, position);
|
this.scripts.set(selection.script, position);
|
||||||
});
|
});
|
||||||
@@ -35,13 +37,13 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
return this.scripts.size === 0;
|
return this.scripts.size === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
public getScriptPositionInCode(script: Script): ICodePosition {
|
||||||
return this.getPositionById(script.id);
|
return this.getPositionById(script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPositionById(scriptId: string): ICodePosition {
|
private getPositionById(scriptId: ExecutableId): ICodePosition {
|
||||||
const position = [...this.scripts.entries()]
|
const position = [...this.scripts.entries()]
|
||||||
.filter(([s]) => s.id === scriptId)
|
.filter(([s]) => s.executableId === scriptId)
|
||||||
.map(([, pos]) => pos)
|
.map(([, pos]) => pos)
|
||||||
.at(0);
|
.at(0);
|
||||||
if (!position) {
|
if (!position) {
|
||||||
@@ -52,12 +54,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
const totalLines = splitTextIntoLines(script).length;
|
||||||
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||||
if (missingPositions.length > 0) {
|
if (missingPositions.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||||
+ `(total code lines: ${totalLines}).`,
|
+ ` (total code lines: ${totalLines}).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +67,7 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
|
|||||||
function getChangedScripts(
|
function getChangedScripts(
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
newScripts: ReadonlyArray<SelectedScript>,
|
newScripts: ReadonlyArray<SelectedScript>,
|
||||||
): ReadonlyArray<IScript> {
|
): ReadonlyArray<Script> {
|
||||||
return newScripts
|
return newScripts
|
||||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
&& oldScript.revert !== newScript.revert))
|
&& oldScript.revert !== newScript.revert))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { IScript } from '@/domain/IScript';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
readonly addedScripts: ReadonlyArray<IScript>;
|
readonly addedScripts: ReadonlyArray<Script>;
|
||||||
readonly removedScripts: ReadonlyArray<IScript>;
|
readonly removedScripts: ReadonlyArray<Script>;
|
||||||
readonly changedScripts: ReadonlyArray<IScript>;
|
readonly changedScripts: ReadonlyArray<Script>;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
getScriptPositionInCode(script: Script): ICodePosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||||
import type { ICodeBuilder } from './ICodeBuilder';
|
import type { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
@@ -15,7 +16,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
this.lines.push('');
|
this.lines.push('');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = splitTextIntoLines(code);
|
||||||
if (lines) {
|
if (lines) {
|
||||||
this.lines.push(...lines);
|
this.lines.push(...lines);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { FilterChange } from './Event/FilterChange';
|
import { FilterChange } from './Event/FilterChange';
|
||||||
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
||||||
import type { FilterResult } from './Result/FilterResult';
|
import type { FilterResult } from './Result/FilterResult';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { IScript } from '@/domain/IScript';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { ICategory } from '@/domain/ICategory';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { FilterResult } from './FilterResult';
|
import type { FilterResult } from './FilterResult';
|
||||||
|
|
||||||
export class AppliedFilterResult implements FilterResult {
|
export class AppliedFilterResult implements FilterResult {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
public readonly scriptMatches: ReadonlyArray<Script>,
|
||||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
public readonly categoryMatches: ReadonlyArray<Category>,
|
||||||
public readonly query: string,
|
public readonly query: string,
|
||||||
) {
|
) {
|
||||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { IScript, ICategory } from '@/domain/ICategory';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
|
||||||
export interface FilterResult {
|
export interface FilterResult {
|
||||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
readonly categoryMatches: ReadonlyArray<Category>;
|
||||||
readonly scriptMatches: ReadonlyArray<IScript>;
|
readonly scriptMatches: ReadonlyArray<Script>;
|
||||||
readonly query: string;
|
readonly query: string;
|
||||||
hasAnyMatches(): boolean;
|
hasAnyMatches(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { FilterResult } from '../Result/FilterResult';
|
import type { FilterResult } from '../Result/FilterResult';
|
||||||
|
|
||||||
export interface FilterStrategy {
|
export interface FilterStrategy {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ICategory, IScript } from '@/domain/ICategory';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
import type { IDocumentable } from '@/domain/IDocumentable';
|
import type { Documentable } from '@/domain/Executables/Documentable';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
||||||
import type { FilterStrategy } from './FilterStrategy';
|
import type { FilterStrategy } from './FilterStrategy';
|
||||||
import type { FilterResult } from '../Result/FilterResult';
|
import type { FilterResult } from '../Result/FilterResult';
|
||||||
@@ -24,7 +25,7 @@ export class LinearFilterStrategy implements FilterStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesCategory(
|
function matchesCategory(
|
||||||
category: ICategory,
|
category: Category,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return matchesAny(
|
return matchesAny(
|
||||||
@@ -34,7 +35,7 @@ function matchesCategory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesScript(
|
function matchesScript(
|
||||||
script: IScript,
|
script: Script,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return matchesAny(
|
return matchesAny(
|
||||||
@@ -58,7 +59,7 @@ function matchName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchCode(
|
function matchCode(
|
||||||
code: IScriptCode,
|
code: ScriptCode,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (code.execute.toLowerCase().includes(filterLowercase)) {
|
if (code.execute.toLowerCase().includes(filterLowercase)) {
|
||||||
@@ -71,7 +72,7 @@ function matchCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchDocumentation(
|
function matchDocumentation(
|
||||||
documentable: IDocumentable,
|
documentable: Documentable,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return documentable.docs.some(
|
return documentable.docs.some(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { IApplicationCode } from './Code/IApplicationCode';
|
import type { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ICategory } from '@/domain/ICategory';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
export interface ReadonlyCategorySelection {
|
export interface ReadonlyCategorySelection {
|
||||||
areAllScriptsSelected(category: ICategory): boolean;
|
areAllScriptsSelected(category: Category): boolean;
|
||||||
isAnyScriptSelected(category: ICategory): boolean;
|
isAnyScriptSelected(category: Category): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategorySelection extends ReadonlyCategorySelection {
|
export interface CategorySelection extends ReadonlyCategorySelection {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
type CategorySelectionStatus = {
|
type CategorySelectionStatus = {
|
||||||
readonly isSelected: true;
|
readonly isSelected: true;
|
||||||
readonly isReverted: boolean;
|
readonly isReverted: boolean;
|
||||||
@@ -6,7 +8,7 @@ type CategorySelectionStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface CategorySelectionChange {
|
export interface CategorySelectionChange {
|
||||||
readonly categoryId: number;
|
readonly categoryId: ExecutableId;
|
||||||
readonly newStatus: CategorySelectionStatus;
|
readonly newStatus: CategorySelectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ICategory } from '@/domain/ICategory';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
import type { CategorySelection } from './CategorySelection';
|
import type { CategorySelection } from './CategorySelection';
|
||||||
import type { ScriptSelection } from '../Script/ScriptSelection';
|
import type { ScriptSelection } from '../Script/ScriptSelection';
|
||||||
@@ -13,7 +13,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public areAllScriptsSelected(category: ICategory): boolean {
|
public areAllScriptsSelected(category: Category): boolean {
|
||||||
const { selectedScripts } = this.scriptSelection;
|
const { selectedScripts } = this.scriptSelection;
|
||||||
if (selectedScripts.length === 0) {
|
if (selectedScripts.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -23,11 +23,11 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return scripts.every(
|
return scripts.every(
|
||||||
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
(script) => selectedScripts.some((selected) => selected.id === script.executableId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAnyScriptSelected(category: ICategory): boolean {
|
public isAnyScriptSelected(category: Category): boolean {
|
||||||
const { selectedScripts } = this.scriptSelection;
|
const { selectedScripts } = this.scriptSelection;
|
||||||
if (selectedScripts.length === 0) {
|
if (selectedScripts.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
const scripts = category.getAllScriptsRecursively();
|
const scripts = category.getAllScriptsRecursively();
|
||||||
const scriptsChangesInCategory = scripts
|
const scriptsChangesInCategory = scripts
|
||||||
.map((script): ScriptSelectionChange => ({
|
.map((script): ScriptSelectionChange => ({
|
||||||
scriptId: script.id,
|
scriptId: script.executableId,
|
||||||
newStatus: {
|
newStatus: {
|
||||||
...change.newStatus,
|
...change.newStatus,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import type { IScript } from '@/domain/IScript';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import { UserSelectedScript } from './UserSelectedScript';
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
import type { ScriptSelection } from './ScriptSelection';
|
import type { ScriptSelection } from './ScriptSelection';
|
||||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
@@ -16,7 +17,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
|
|||||||
export class DebouncedScriptSelection implements ScriptSelection {
|
export class DebouncedScriptSelection implements ScriptSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
|
|
||||||
private readonly scripts: Repository<string, SelectedScript>;
|
private readonly scripts: Repository<SelectedScript>;
|
||||||
|
|
||||||
public readonly processChanges: ScriptSelection['processChanges'];
|
public readonly processChanges: ScriptSelection['processChanges'];
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
debounce: DebounceFunction = batchedDebounce,
|
debounce: DebounceFunction = batchedDebounce,
|
||||||
) {
|
) {
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
this.scripts = new InMemoryRepository<SelectedScript>();
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
@@ -38,8 +39,8 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSelected(scriptId: string): boolean {
|
public isSelected(scriptExecutableId: ExecutableId): boolean {
|
||||||
return this.scripts.exists(scriptId);
|
return this.scripts.exists(scriptExecutableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get selectedScripts(): readonly SelectedScript[] {
|
public get selectedScripts(): readonly SelectedScript[] {
|
||||||
@@ -49,7 +50,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
const scriptsToSelect = this.collection
|
const scriptsToSelect = this.collection
|
||||||
.getAllScripts()
|
.getAllScripts()
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
.filter((script) => !this.scripts.exists(script.executableId))
|
||||||
.map((script) => new UserSelectedScript(script, false));
|
.map((script) => new UserSelectedScript(script, false));
|
||||||
if (scriptsToSelect.length === 0) {
|
if (scriptsToSelect.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -80,7 +81,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectOnly(scripts: readonly IScript[]): void {
|
public selectOnly(scripts: readonly Script[]): void {
|
||||||
assertNonEmptyScriptSelection(scripts);
|
assertNonEmptyScriptSelection(scripts);
|
||||||
this.processChanges({
|
this.processChanges({
|
||||||
changes: [
|
changes: [
|
||||||
@@ -116,12 +117,12 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
private applyChange(change: ScriptSelectionChange): number {
|
private applyChange(change: ScriptSelectionChange): number {
|
||||||
const script = this.collection.getScript(change.scriptId);
|
const script = this.collection.getScript(change.scriptId);
|
||||||
if (change.newStatus.isSelected) {
|
if (change.newStatus.isSelected) {
|
||||||
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted);
|
||||||
}
|
}
|
||||||
return this.removeScript(script.id);
|
return this.removeScript(script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
private addOrUpdateScript(scriptId: ExecutableId, revert: boolean): number {
|
||||||
const script = this.collection.getScript(scriptId);
|
const script = this.collection.getScript(scriptId);
|
||||||
const selectedScript = new UserSelectedScript(script, revert);
|
const selectedScript = new UserSelectedScript(script, revert);
|
||||||
if (!this.scripts.exists(selectedScript.id)) {
|
if (!this.scripts.exists(selectedScript.id)) {
|
||||||
@@ -136,7 +137,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeScript(scriptId: string): number {
|
private removeScript(scriptId: ExecutableId): number {
|
||||||
if (!this.scripts.exists(scriptId)) {
|
if (!this.scripts.exists(scriptId)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -145,31 +146,31 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) {
|
function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
|
||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScriptIdsToBeSelected(
|
function getScriptIdsToBeSelected(
|
||||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
existingItems: ReadonlyRepository<SelectedScript>,
|
||||||
desiredScripts: readonly IScript[],
|
desiredScripts: readonly Script[],
|
||||||
): string[] {
|
): string[] {
|
||||||
return desiredScripts
|
return desiredScripts
|
||||||
.filter((script) => !existingItems.exists(script.id))
|
.filter((script) => !existingItems.exists(script.executableId))
|
||||||
.map((script) => script.id);
|
.map((script) => script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScriptIdsToBeDeselected(
|
function getScriptIdsToBeDeselected(
|
||||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
existingItems: ReadonlyRepository<SelectedScript>,
|
||||||
desiredScripts: readonly IScript[],
|
desiredScripts: readonly Script[],
|
||||||
): string[] {
|
): string[] {
|
||||||
return existingItems
|
return existingItems
|
||||||
.getItems()
|
.getItems()
|
||||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
|
||||||
.map((script) => script.id);
|
.map((script) => script.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||||
return a.script.equals(b.script.id) && a.revert === b.revert;
|
return a.script.executableId === b.script.executableId && a.revert === b.revert;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import type { IScript } from '@/domain/IScript';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import type { SelectedScript } from './SelectedScript';
|
import type { SelectedScript } from './SelectedScript';
|
||||||
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
|
||||||
export interface ReadonlyScriptSelection {
|
export interface ReadonlyScriptSelection {
|
||||||
readonly changed: IEventSource<readonly SelectedScript[]>;
|
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||||
readonly selectedScripts: readonly SelectedScript[];
|
readonly selectedScripts: readonly SelectedScript[];
|
||||||
isSelected(scriptId: string): boolean;
|
isSelected(scriptExecutableId: ExecutableId): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptSelection extends ReadonlyScriptSelection {
|
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||||
selectOnly(scripts: readonly IScript[]): void;
|
selectOnly(scripts: readonly Script[]): void;
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
deselectAll(): void;
|
deselectAll(): void;
|
||||||
processChanges(action: ScriptSelectionChangeCommand): void;
|
processChanges(action: ScriptSelectionChangeCommand): void;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
export type ScriptSelectionStatus = {
|
export type ScriptSelectionStatus = {
|
||||||
readonly isSelected: true;
|
readonly isSelected: true;
|
||||||
readonly isReverted: boolean;
|
readonly isReverted: boolean;
|
||||||
@@ -7,7 +9,7 @@ export type ScriptSelectionStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ScriptSelectionChange {
|
export interface ScriptSelectionChange {
|
||||||
readonly scriptId: string;
|
readonly scriptId: ExecutableId;
|
||||||
readonly newStatus: ScriptSelectionStatus;
|
readonly newStatus: ScriptSelectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { IScript } from '@/domain/IScript';
|
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||||
|
|
||||||
type ScriptId = IScript['id'];
|
export interface SelectedScript extends RepositoryEntity {
|
||||||
|
readonly script: Script;
|
||||||
export interface SelectedScript extends IEntity<ScriptId> {
|
|
||||||
readonly script: IScript;
|
|
||||||
readonly revert: boolean;
|
readonly revert: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { IScript } from '@/domain/IScript';
|
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||||
import type { SelectedScript } from './SelectedScript';
|
|
||||||
|
|
||||||
type SelectedScriptId = SelectedScript['id'];
|
export class UserSelectedScript implements RepositoryEntity {
|
||||||
|
public readonly id: string;
|
||||||
|
|
||||||
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: IScript,
|
public readonly script: Script,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
super(script.id);
|
this.id = script.executableId;
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||||
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||||
import type { CategorySelection } from './Category/CategorySelection';
|
import type { CategorySelection } from './Category/CategorySelection';
|
||||||
|
|||||||
@@ -1,40 +1,48 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
|
||||||
import type { 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 LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
|
||||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
categoryParser = parseCategoryCollection,
|
collectionsData: readonly CollectionData[] = PreParsedCollections,
|
||||||
projectDetailsParser = parseProjectDetails,
|
utilities: ApplicationParserUtilities = DefaultUtilities,
|
||||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
|
||||||
collectionsData = PreParsedCollections,
|
|
||||||
): IApplication {
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData);
|
validateCollectionsData(collectionsData, utilities.validator);
|
||||||
const projectDetails = projectDetailsParser(metadata);
|
const projectDetails = utilities.parseProjectDetails();
|
||||||
const collections = collectionsData.map(
|
const collections = collectionsData.map(
|
||||||
(collection) => categoryParser(collection, projectDetails),
|
(collection) => utilities.parseCategoryCollection(collection, projectDetails),
|
||||||
);
|
);
|
||||||
const app = new Application(projectDetails, collections);
|
const app = new Application(projectDetails, collections);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryCollectionParserType
|
const PreParsedCollections: readonly CollectionData[] = [
|
||||||
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
|
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData [] = [
|
|
||||||
WindowsData, MacOsData, LinuxData,
|
WindowsData, MacOsData, LinuxData,
|
||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(
|
||||||
if (!collections.length) {
|
collections: readonly CollectionData[],
|
||||||
throw new Error('missing collections');
|
validator: TypeValidator,
|
||||||
}
|
) {
|
||||||
|
validator.assertNonEmptyCollection({
|
||||||
|
value: collections,
|
||||||
|
valueName: 'Collections',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApplicationParserUtilities {
|
||||||
|
readonly parseCategoryCollection: CategoryCollectionParser;
|
||||||
|
readonly validator: TypeValidator;
|
||||||
|
readonly parseProjectDetails: ProjectDetailsParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultUtilities: ApplicationParserUtilities = {
|
||||||
|
parseCategoryCollection,
|
||||||
|
parseProjectDetails,
|
||||||
|
validator: createTypeValidator(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,34 +1,75 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||||
|
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export const parseCategoryCollection: CategoryCollectionParser = (
|
||||||
content: CollectionData,
|
content,
|
||||||
projectDetails: ProjectDetails,
|
projectDetails,
|
||||||
osParser = createEnumParser(OperatingSystem),
|
utilities: CategoryCollectionParserUtilities = DefaultUtilities,
|
||||||
): ICategoryCollection {
|
) => {
|
||||||
validate(content);
|
validateCollection(content, utilities.validator);
|
||||||
const scripting = new ScriptingDefinitionParser()
|
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
|
||||||
.parse(content.scripting, projectDetails);
|
const collectionUtilities = utilities.createUtilities(content.functions, scripting);
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
const categories = content.actions.map(
|
||||||
const categories = content.actions.map((action) => parseCategory(action, context));
|
(action) => utilities.parseCategory(action, collectionUtilities),
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
|
||||||
const collection = new CategoryCollection(
|
|
||||||
os,
|
|
||||||
categories,
|
|
||||||
scripting,
|
|
||||||
);
|
);
|
||||||
|
const os = utilities.osParser.parseEnum(content.os, 'os');
|
||||||
|
const collection = utilities.createCategoryCollection({
|
||||||
|
os, actions: categories, scripting,
|
||||||
|
});
|
||||||
return collection;
|
return collection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CategoryCollectionFactory = (
|
||||||
|
...parameters: ConstructorParameters<typeof CategoryCollection>
|
||||||
|
) => ICategoryCollection;
|
||||||
|
|
||||||
|
export interface CategoryCollectionParser {
|
||||||
|
(
|
||||||
|
content: CollectionData,
|
||||||
|
projectDetails: ProjectDetails,
|
||||||
|
utilities?: CategoryCollectionParserUtilities,
|
||||||
|
): ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validateCollection(
|
||||||
if (!content.actions.length) {
|
content: CollectionData,
|
||||||
throw new Error('content does not define any action');
|
validator: TypeValidator,
|
||||||
}
|
): void {
|
||||||
|
validator.assertObject({
|
||||||
|
value: content,
|
||||||
|
valueName: 'Collection',
|
||||||
|
allowedProperties: [
|
||||||
|
'os', 'scripting', 'actions', 'functions',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
validator.assertNonEmptyCollection({
|
||||||
|
value: content.actions,
|
||||||
|
valueName: '\'actions\' in collection',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CategoryCollectionParserUtilities {
|
||||||
|
readonly osParser: EnumParser<OperatingSystem>;
|
||||||
|
readonly validator: TypeValidator;
|
||||||
|
readonly parseScriptingDefinition: ScriptingDefinitionParser;
|
||||||
|
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
|
||||||
|
readonly parseCategory: CategoryParser;
|
||||||
|
readonly createCategoryCollection: CategoryCollectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultUtilities: CategoryCollectionParserUtilities = {
|
||||||
|
osParser: createEnumParser(OperatingSystem),
|
||||||
|
validator: createTypeValidator(),
|
||||||
|
parseScriptingDefinition,
|
||||||
|
createUtilities: createCollectionUtilities,
|
||||||
|
parseCategory,
|
||||||
|
createCategoryCollection: (...args) => new CategoryCollection(...args),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import type {
|
|
||||||
CategoryData, ScriptData, CategoryOrScriptData,
|
|
||||||
} from '@/application/collections/';
|
|
||||||
import { Script } from '@/domain/Script';
|
|
||||||
import { Category } from '@/domain/Category';
|
|
||||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
|
||||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
|
||||||
import { parseDocs } from './DocumentationParser';
|
|
||||||
import { parseScript } from './Script/ScriptParser';
|
|
||||||
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
|
||||||
|
|
||||||
let categoryIdCounter = 0;
|
|
||||||
|
|
||||||
export function parseCategory(
|
|
||||||
category: CategoryData,
|
|
||||||
context: ICategoryCollectionParseContext,
|
|
||||||
factory: CategoryFactoryType = CategoryFactory,
|
|
||||||
): Category {
|
|
||||||
return parseCategoryRecursively({
|
|
||||||
categoryData: category,
|
|
||||||
context,
|
|
||||||
factory,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICategoryParseContext {
|
|
||||||
readonly categoryData: CategoryData,
|
|
||||||
readonly context: ICategoryCollectionParseContext,
|
|
||||||
readonly factory: CategoryFactoryType,
|
|
||||||
readonly parentCategory?: CategoryData,
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
|
||||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
|
||||||
const children: ICategoryChildren = {
|
|
||||||
subCategories: new Array<Category>(),
|
|
||||||
subScripts: new Array<Script>(),
|
|
||||||
};
|
|
||||||
for (const data of context.categoryData.children) {
|
|
||||||
parseNode({
|
|
||||||
nodeData: data,
|
|
||||||
children,
|
|
||||||
parent: context.categoryData,
|
|
||||||
factory: context.factory,
|
|
||||||
context: context.context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return context.factory(
|
|
||||||
/* id: */ categoryIdCounter++,
|
|
||||||
/* name: */ context.categoryData.category,
|
|
||||||
/* docs: */ parseDocs(context.categoryData),
|
|
||||||
/* categories: */ children.subCategories,
|
|
||||||
/* scripts: */ children.subScripts,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
return new NodeValidator({
|
|
||||||
type: NodeType.Category,
|
|
||||||
selfNode: context.categoryData,
|
|
||||||
parentNode: context.parentCategory,
|
|
||||||
}).throw(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
|
||||||
new NodeValidator({
|
|
||||||
type: NodeType.Category,
|
|
||||||
selfNode: category,
|
|
||||||
parentNode: parentCategory,
|
|
||||||
})
|
|
||||||
.assertDefined(category)
|
|
||||||
.assertValidName(category.category)
|
|
||||||
.assert(
|
|
||||||
() => category.children.length > 0,
|
|
||||||
`"${category.category}" has no children.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICategoryChildren {
|
|
||||||
subCategories: Category[];
|
|
||||||
subScripts: Script[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface INodeParseContext {
|
|
||||||
readonly nodeData: CategoryOrScriptData;
|
|
||||||
readonly children: ICategoryChildren;
|
|
||||||
readonly parent: CategoryData;
|
|
||||||
readonly factory: CategoryFactoryType;
|
|
||||||
readonly context: ICategoryCollectionParseContext;
|
|
||||||
}
|
|
||||||
function parseNode(context: INodeParseContext) {
|
|
||||||
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
|
||||||
validator.assertDefined(context.nodeData);
|
|
||||||
if (isCategory(context.nodeData)) {
|
|
||||||
const subCategory = parseCategoryRecursively({
|
|
||||||
categoryData: context.nodeData,
|
|
||||||
context: context.context,
|
|
||||||
factory: context.factory,
|
|
||||||
parentCategory: context.parent,
|
|
||||||
});
|
|
||||||
context.children.subCategories.push(subCategory);
|
|
||||||
} else if (isScript(context.nodeData)) {
|
|
||||||
const script = parseScript(context.nodeData, context.context);
|
|
||||||
context.children.subScripts.push(script);
|
|
||||||
} else {
|
|
||||||
validator.throw('Node is neither a category or a script.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
|
||||||
return hasCode(data) || hasCall(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
|
||||||
return hasProperty(data, 'category');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCode(data: unknown): boolean {
|
|
||||||
return hasProperty(data, 'code');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCall(data: unknown) {
|
|
||||||
return hasProperty(data, 'call');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasProperty(object: unknown, propertyName: string) {
|
|
||||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CategoryFactoryType = (
|
|
||||||
...parameters: ConstructorParameters<typeof Category>) => Category;
|
|
||||||
|
|
||||||
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
|
||||||
116
src/application/Parser/Common/ContextualError.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
|
import { indentText } from '@/application/Common/Text/IndentText';
|
||||||
|
|
||||||
|
export interface ErrorWithContextWrapper {
|
||||||
|
(
|
||||||
|
innerError: Error,
|
||||||
|
additionalContext: string,
|
||||||
|
): Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||||
|
innerError,
|
||||||
|
additionalContext,
|
||||||
|
) => {
|
||||||
|
if (!additionalContext) {
|
||||||
|
throw new Error('Missing additional context');
|
||||||
|
}
|
||||||
|
return new ContextualError({
|
||||||
|
innerError,
|
||||||
|
additionalContext,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for building a detailed error trace.
|
||||||
|
*
|
||||||
|
* Alternatives considered:
|
||||||
|
* - `AggregateError`:
|
||||||
|
* Similar but not well-serialized or displayed by browsers such as Chromium (last tested v126).
|
||||||
|
* - `cause` property:
|
||||||
|
* Not displayed by all browsers (last tested v126).
|
||||||
|
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
||||||
|
*
|
||||||
|
* This is immutable where the constructor sets the values because using getter functions such as
|
||||||
|
* `get cause()`, `get message()` does not work on Chromium (last tested v126), but works fine on
|
||||||
|
* Firefox (last tested v127).
|
||||||
|
*/
|
||||||
|
class ContextualError extends CustomError {
|
||||||
|
constructor(public readonly context: ErrorContext) {
|
||||||
|
super(
|
||||||
|
generateDetailedErrorMessageWithContext(context),
|
||||||
|
{
|
||||||
|
cause: context.innerError,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorContext {
|
||||||
|
readonly innerError: Error;
|
||||||
|
readonly additionalContext: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDetailedErrorMessageWithContext(
|
||||||
|
context: ErrorContext,
|
||||||
|
): string {
|
||||||
|
return [
|
||||||
|
'\n',
|
||||||
|
// Display the current error message first, then the root cause.
|
||||||
|
// This prevents repetitive main messages for errors with a `cause:` chain,
|
||||||
|
// aligning with browser error display conventions.
|
||||||
|
context.additionalContext,
|
||||||
|
'\n',
|
||||||
|
'Error Trace (starting from root cause):',
|
||||||
|
indentText(
|
||||||
|
formatErrorTrace(
|
||||||
|
// Displaying contexts from the top frame (deepest, most recent) aligns with
|
||||||
|
// common debugger/compiler standard.
|
||||||
|
extractErrorTraceAscendingFromDeepest(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'\n',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractErrorTraceAscendingFromDeepest(
|
||||||
|
context: ErrorContext,
|
||||||
|
): string[] {
|
||||||
|
const originalError = findRootError(context.innerError);
|
||||||
|
const contextsDescendingFromMostRecent: string[] = [
|
||||||
|
context.additionalContext,
|
||||||
|
...gatherContextsFromErrorChain(context.innerError),
|
||||||
|
originalError.toString(),
|
||||||
|
];
|
||||||
|
const contextsAscendingFromDeepest = contextsDescendingFromMostRecent.reverse();
|
||||||
|
return contextsAscendingFromDeepest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRootError(error: Error): Error {
|
||||||
|
if (error instanceof ContextualError) {
|
||||||
|
return findRootError(error.context.innerError);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gatherContextsFromErrorChain(
|
||||||
|
error: Error,
|
||||||
|
accumulatedContexts: string[] = [],
|
||||||
|
): string[] {
|
||||||
|
if (error instanceof ContextualError) {
|
||||||
|
accumulatedContexts.push(error.context.additionalContext);
|
||||||
|
return gatherContextsFromErrorChain(error.context.innerError, accumulatedContexts);
|
||||||
|
}
|
||||||
|
return accumulatedContexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorTrace(
|
||||||
|
errorMessages: readonly string[],
|
||||||
|
): string {
|
||||||
|
if (errorMessages.length === 1) {
|
||||||
|
return errorMessages[0];
|
||||||
|
}
|
||||||
|
return errorMessages
|
||||||
|
.map((context, index) => `${index + 1}.${indentText(context)}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
131
src/application/Parser/Common/TypeValidator.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import type { PropertyKeys } from '@/TypeHelpers';
|
||||||
|
import {
|
||||||
|
isNullOrUndefined, isArray, isPlainObject, isString,
|
||||||
|
} from '@/TypeHelpers';
|
||||||
|
|
||||||
|
export interface TypeValidator {
|
||||||
|
assertObject<T>(assertion: ObjectAssertion<T>): void;
|
||||||
|
assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void;
|
||||||
|
assertNonEmptyString(assertion: NonEmptyStringAssertion): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NonEmptyCollectionAssertion {
|
||||||
|
readonly value: unknown;
|
||||||
|
readonly valueName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegexValidationRule {
|
||||||
|
readonly expectedMatch: RegExp;
|
||||||
|
readonly errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NonEmptyStringAssertion {
|
||||||
|
readonly value: unknown;
|
||||||
|
readonly valueName: string;
|
||||||
|
readonly rule?: RegexValidationRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectAssertion<T> {
|
||||||
|
readonly value: T | unknown;
|
||||||
|
readonly valueName: string;
|
||||||
|
readonly allowedProperties?: readonly PropertyKeys<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTypeValidator(): TypeValidator {
|
||||||
|
return {
|
||||||
|
assertObject: (assertion) => {
|
||||||
|
assertDefined(assertion.value, assertion.valueName);
|
||||||
|
assertPlainObject(assertion.value, assertion.valueName);
|
||||||
|
assertNoEmptyProperties(assertion.value, assertion.valueName);
|
||||||
|
if (assertion.allowedProperties !== undefined) {
|
||||||
|
const allowedProperties = assertion.allowedProperties.map((p) => p as string);
|
||||||
|
assertAllowedProperties(assertion.value, assertion.valueName, allowedProperties);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertNonEmptyCollection: (assertion) => {
|
||||||
|
assertDefined(assertion.value, assertion.valueName);
|
||||||
|
assertArray(assertion.value, assertion.valueName);
|
||||||
|
assertNonEmpty(assertion.value, assertion.valueName);
|
||||||
|
},
|
||||||
|
assertNonEmptyString: (assertion) => {
|
||||||
|
assertDefined(assertion.value, assertion.valueName);
|
||||||
|
assertString(assertion.value, assertion.valueName);
|
||||||
|
if (assertion.value.length === 0) {
|
||||||
|
throw new Error(`'${assertion.valueName}' is missing.`);
|
||||||
|
}
|
||||||
|
if (assertion.rule) {
|
||||||
|
if (!assertion.value.match(assertion.rule.expectedMatch)) {
|
||||||
|
throw new Error(assertion.rule.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertDefined<T>(
|
||||||
|
value: T,
|
||||||
|
valueName: string,
|
||||||
|
): asserts value is NonNullable<T> {
|
||||||
|
if (isNullOrUndefined(value)) {
|
||||||
|
throw new Error(`'${valueName}' is missing.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPlainObject(
|
||||||
|
value: unknown,
|
||||||
|
valueName: string,
|
||||||
|
): asserts value is object {
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
throw new Error(`'${valueName}' is not an object.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNoEmptyProperties(
|
||||||
|
value: object,
|
||||||
|
valueName: string,
|
||||||
|
): void {
|
||||||
|
if (Object.keys(value).length === 0) {
|
||||||
|
throw new Error(`'${valueName}' is an empty object without properties.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAllowedProperties(
|
||||||
|
value: object,
|
||||||
|
valueName: string,
|
||||||
|
allowedProperties: readonly string[],
|
||||||
|
): void {
|
||||||
|
const properties = Object.keys(value).map((p) => p as string);
|
||||||
|
const disallowedProperties = properties.filter(
|
||||||
|
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
|
||||||
|
);
|
||||||
|
if (disallowedProperties.length > 0) {
|
||||||
|
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertArray(
|
||||||
|
value: unknown,
|
||||||
|
valueName: string,
|
||||||
|
): asserts value is Array<unknown> {
|
||||||
|
if (!isArray(value)) {
|
||||||
|
throw new Error(`${valueName} should be of type 'array', but is of type '${typeof value}'.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertString(
|
||||||
|
value: unknown,
|
||||||
|
valueName: string,
|
||||||
|
): asserts value is string {
|
||||||
|
if (!isString(value)) {
|
||||||
|
throw new Error(`${valueName} should be of type 'string', but is of type '${typeof value}'.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNonEmpty(
|
||||||
|
value: Array<unknown>,
|
||||||
|
valueName: string,
|
||||||
|
): void {
|
||||||
|
if (value.length === 0) {
|
||||||
|
throw new Error(`'${valueName}' cannot be an empty array.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import type { FunctionData } from '@/application/collections/';
|
||||||
|
import { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
|
||||||
|
import { SyntaxFactory } from './Script/Validation/Syntax/SyntaxFactory';
|
||||||
|
import type { IScriptCompiler } from './Script/Compiler/IScriptCompiler';
|
||||||
|
import type { ILanguageSyntax } from './Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import type { ISyntaxFactory } from './Script/Validation/Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
|
export interface CategoryCollectionSpecificUtilities {
|
||||||
|
readonly compiler: IScriptCompiler;
|
||||||
|
readonly syntax: ILanguageSyntax;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFactory = (
|
||||||
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
|
scripting: IScriptingDefinition,
|
||||||
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
|
) => {
|
||||||
|
const syntax = syntaxFactory.create(scripting.language);
|
||||||
|
return {
|
||||||
|
compiler: new ScriptCompiler({
|
||||||
|
functions: functionsData ?? [],
|
||||||
|
syntax,
|
||||||
|
}),
|
||||||
|
syntax,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CategoryCollectionSpecificUtilitiesFactory {
|
||||||
|
(
|
||||||
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
|
scripting: IScriptingDefinition,
|
||||||
|
syntaxFactory?: ISyntaxFactory,
|
||||||
|
): CategoryCollectionSpecificUtilities;
|
||||||
|
}
|
||||||
181
src/application/Parser/Executable/CategoryParser.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import type {
|
||||||
|
CategoryData, ScriptData, ExecutableData,
|
||||||
|
} from '@/application/collections/';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
||||||
|
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||||
|
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||||
|
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
||||||
|
import { ExecutableType } from './Validation/ExecutableType';
|
||||||
|
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
||||||
|
|
||||||
|
export const parseCategory: CategoryParser = (
|
||||||
|
category: CategoryData,
|
||||||
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
|
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||||
|
) => {
|
||||||
|
return parseCategoryRecursively({
|
||||||
|
categoryData: category,
|
||||||
|
collectionUtilities,
|
||||||
|
categoryUtilities,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CategoryParser {
|
||||||
|
(
|
||||||
|
category: CategoryData,
|
||||||
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
|
categoryUtilities?: CategoryParserUtilities,
|
||||||
|
): Category;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryParseContext {
|
||||||
|
readonly categoryData: CategoryData;
|
||||||
|
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
||||||
|
readonly parentCategory?: CategoryData;
|
||||||
|
readonly categoryUtilities: CategoryParserUtilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCategoryRecursively(
|
||||||
|
context: CategoryParseContext,
|
||||||
|
): Category | never {
|
||||||
|
const validator = ensureValidCategory(context);
|
||||||
|
const children: CategoryChildren = {
|
||||||
|
subcategories: new Array<Category>(),
|
||||||
|
subscripts: new Array<Script>(),
|
||||||
|
};
|
||||||
|
for (const data of context.categoryData.children) {
|
||||||
|
parseUnknownExecutable({
|
||||||
|
data,
|
||||||
|
children,
|
||||||
|
parent: context.categoryData,
|
||||||
|
categoryUtilities: context.categoryUtilities,
|
||||||
|
collectionUtilities: context.collectionUtilities,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return context.categoryUtilities.createCategory({
|
||||||
|
executableId: context.categoryData.category, // Pseudo-ID for uniqueness until real ID support
|
||||||
|
name: context.categoryData.category,
|
||||||
|
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
||||||
|
subcategories: children.subcategories,
|
||||||
|
scripts: children.subscripts,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw context.categoryUtilities.wrapError(
|
||||||
|
error,
|
||||||
|
validator.createContextualErrorMessage('Failed to parse category.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCategory(
|
||||||
|
context: CategoryParseContext,
|
||||||
|
): ExecutableValidator {
|
||||||
|
const category = context.categoryData;
|
||||||
|
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
||||||
|
type: ExecutableType.Category,
|
||||||
|
self: context.categoryData,
|
||||||
|
parentCategory: context.parentCategory,
|
||||||
|
});
|
||||||
|
validator.assertType((v) => v.assertObject({
|
||||||
|
value: category,
|
||||||
|
valueName: `Category '${category.category}'` ?? 'Category',
|
||||||
|
allowedProperties: [
|
||||||
|
'docs', 'children', 'category',
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
validator.assertValidName(category.category);
|
||||||
|
validator.assertType((v) => v.assertNonEmptyCollection({
|
||||||
|
value: category.children,
|
||||||
|
valueName: category.category,
|
||||||
|
}));
|
||||||
|
return validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryChildren {
|
||||||
|
readonly subcategories: Category[];
|
||||||
|
readonly subscripts: Script[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExecutableParseContext {
|
||||||
|
readonly data: ExecutableData;
|
||||||
|
readonly children: CategoryChildren;
|
||||||
|
readonly parent: CategoryData;
|
||||||
|
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
||||||
|
readonly categoryUtilities: CategoryParserUtilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUnknownExecutable(context: ExecutableParseContext) {
|
||||||
|
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
||||||
|
self: context.data,
|
||||||
|
parentCategory: context.parent,
|
||||||
|
});
|
||||||
|
validator.assertType((v) => v.assertObject({
|
||||||
|
value: context.data,
|
||||||
|
valueName: 'Executable',
|
||||||
|
}));
|
||||||
|
validator.assert(
|
||||||
|
() => isCategory(context.data) || isScript(context.data),
|
||||||
|
'Executable is neither a category or a script.',
|
||||||
|
);
|
||||||
|
if (isCategory(context.data)) {
|
||||||
|
const subCategory = parseCategoryRecursively({
|
||||||
|
categoryData: context.data,
|
||||||
|
collectionUtilities: context.collectionUtilities,
|
||||||
|
parentCategory: context.parent,
|
||||||
|
categoryUtilities: context.categoryUtilities,
|
||||||
|
});
|
||||||
|
context.children.subcategories.push(subCategory);
|
||||||
|
} else { // A script
|
||||||
|
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
|
||||||
|
context.children.subscripts.push(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isScript(data: ExecutableData): data is ScriptData {
|
||||||
|
return hasCode(data) || hasCall(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCategory(data: ExecutableData): data is CategoryData {
|
||||||
|
return hasProperty(data, 'category');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCode(data: unknown): boolean {
|
||||||
|
return hasProperty(data, 'code');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(data: unknown) {
|
||||||
|
return hasProperty(data, 'call');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasProperty(
|
||||||
|
object: unknown,
|
||||||
|
propertyName: string,
|
||||||
|
): object is NonNullable<object> {
|
||||||
|
if (typeof object !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (object === null) { // `typeof object` is `null`
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryParserUtilities {
|
||||||
|
readonly createCategory: CategoryFactory;
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createValidator: ExecutableValidatorFactory;
|
||||||
|
readonly parseScript: ScriptParser;
|
||||||
|
readonly parseDocs: DocsParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||||
|
createCategory,
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createValidator: createExecutableDataValidator,
|
||||||
|
parseScript,
|
||||||
|
parseDocs,
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
import { isString, isArray } from '@/TypeHelpers';
|
import { isString, isArray } from '@/TypeHelpers';
|
||||||
|
|
||||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
export const parseDocs: DocsParser = (documentable) => {
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
@@ -9,6 +9,12 @@ export function parseDocs(documentable: DocumentableData): readonly string[] {
|
|||||||
let result = new DocumentationContainer();
|
let result = new DocumentationContainer();
|
||||||
result = addDocs(docs, result);
|
result = addDocs(docs, result);
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DocsParser {
|
||||||
|
(
|
||||||
|
documentable: DocumentableData,
|
||||||
|
): readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(
|
function addDocs(
|
||||||
@@ -44,5 +50,5 @@ class DocumentationContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function throwInvalidType(): never {
|
function throwInvalidType(): never {
|
||||||
throw new Error('docs field (documentation) must be an array of strings');
|
throw new Error('docs field (documentation) must be a single string or an array of strings.');
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
@@ -7,15 +7,18 @@ import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parame
|
|||||||
import type { IExpression } from './IExpression';
|
import type { IExpression } from './IExpression';
|
||||||
|
|
||||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
|
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
|
|
||||||
constructor(
|
public readonly position: ExpressionPosition;
|
||||||
public readonly position: ExpressionPosition,
|
|
||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator;
|
||||||
parameters?: IReadOnlyFunctionParameterCollection,
|
|
||||||
) {
|
constructor(parameters: ExpressionInitParameters) {
|
||||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
this.parameters = parameters.parameters ?? new FunctionParameterCollection();
|
||||||
|
this.evaluator = parameters.evaluator;
|
||||||
|
this.position = parameters.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
@@ -26,6 +29,12 @@ export class Expression implements IExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpressionInitParameters {
|
||||||
|
readonly position: ExpressionPosition,
|
||||||
|
readonly evaluator: ExpressionEvaluator,
|
||||||
|
readonly parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
|
}
|
||||||
|
|
||||||
function validateThatAllRequiredParametersAreSatisfied(
|
function validateThatAllRequiredParametersAreSatisfied(
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
|
||||||
export function createPositionFromRegexFullMatch(
|
export interface ExpressionPositionFactory {
|
||||||
match: RegExpMatchArray,
|
(
|
||||||
): ExpressionPosition {
|
match: RegExpMatchArray,
|
||||||
|
): ExpressionPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPositionFromRegexFullMatch
|
||||||
|
: ExpressionPositionFactory = (match) => {
|
||||||
const startPos = match.index;
|
const startPos = match.index;
|
||||||
if (startPos === undefined) {
|
if (startPos === undefined) {
|
||||||
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||||
@@ -13,4 +18,4 @@ export function createPositionFromRegexFullMatch(
|
|||||||
}
|
}
|
||||||
const endPos = startPos + fullMatch.length;
|
const endPos = startPos + fullMatch.length;
|
||||||
return new ExpressionPosition(startPos, endPos);
|
return new ExpressionPosition(startPos, endPos);
|
||||||
}
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||||
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import type { IExpressionsCompiler } from './IExpressionsCompiler';
|
import type { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||||
@@ -3,10 +3,10 @@ import { WithParser } from '../SyntaxParsers/WithParser';
|
|||||||
import type { IExpression } from '../Expression/IExpression';
|
import type { IExpression } from '../Expression/IExpression';
|
||||||
import type { IExpressionParser } from './IExpressionParser';
|
import type { IExpressionParser } from './IExpressionParser';
|
||||||
|
|
||||||
const Parsers = [
|
const Parsers: readonly IExpressionParser[] = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
new WithParser(),
|
new WithParser(),
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
|
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
|
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||||
|
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
|
import type { IExpressionParser } from '../IExpressionParser';
|
||||||
|
import type { IExpression } from '../../Expression/IExpression';
|
||||||
|
import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter';
|
||||||
|
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
|
export interface RegexParserUtilities {
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createPosition: ExpressionPositionFactory;
|
||||||
|
readonly createExpression: ExpressionFactory;
|
||||||
|
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
|
protected abstract readonly regex: RegExp;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
return Array.from(this.findRegexExpressions(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
|
||||||
|
|
||||||
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error(
|
||||||
|
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const createErrorContext = (message: string): ErrorContext => ({ code, errorMessage: message });
|
||||||
|
const matches = this.doOrRethrow(
|
||||||
|
() => code.matchAll(this.regex),
|
||||||
|
createErrorContext('Failed to match regex.'),
|
||||||
|
);
|
||||||
|
for (const match of matches) {
|
||||||
|
const primitiveExpression = this.doOrRethrow(
|
||||||
|
() => this.buildExpression(match),
|
||||||
|
createErrorContext('Failed to build expression.'),
|
||||||
|
);
|
||||||
|
const position = this.doOrRethrow(
|
||||||
|
() => this.utilities.createPosition(match),
|
||||||
|
createErrorContext('Failed to create position.'),
|
||||||
|
);
|
||||||
|
const parameters = this.doOrRethrow(
|
||||||
|
() => createParameters(
|
||||||
|
primitiveExpression,
|
||||||
|
this.utilities.createParameterCollection(),
|
||||||
|
),
|
||||||
|
createErrorContext('Failed to create parameters.'),
|
||||||
|
);
|
||||||
|
const expression = this.doOrRethrow(
|
||||||
|
() => this.utilities.createExpression({
|
||||||
|
position,
|
||||||
|
evaluator: primitiveExpression.evaluator,
|
||||||
|
parameters,
|
||||||
|
}),
|
||||||
|
createErrorContext('Failed to create expression.'),
|
||||||
|
);
|
||||||
|
yield expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private doOrRethrow<T>(
|
||||||
|
action: () => T,
|
||||||
|
context: ErrorContext,
|
||||||
|
): T {
|
||||||
|
try {
|
||||||
|
return action();
|
||||||
|
} catch (error) {
|
||||||
|
throw this.utilities.wrapError(
|
||||||
|
error,
|
||||||
|
this.buildErrorMessageWithContext(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildErrorMessageWithContext(context: ErrorContext): string {
|
||||||
|
return [
|
||||||
|
context.errorMessage,
|
||||||
|
`Class name: ${this.constructor.name}`,
|
||||||
|
`Regex pattern used: ${this.regex}`,
|
||||||
|
`Code: ${context.code}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorContext {
|
||||||
|
readonly errorMessage: string,
|
||||||
|
readonly code: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParameters(
|
||||||
|
expression: PrimitiveExpression,
|
||||||
|
parameterCollection: IFunctionParameterCollection,
|
||||||
|
): IReadOnlyFunctionParameterCollection {
|
||||||
|
return (expression.parameters || [])
|
||||||
|
.reduce((parameters, parameter) => {
|
||||||
|
parameters.addParameter(parameter);
|
||||||
|
return parameters;
|
||||||
|
}, parameterCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrimitiveExpression {
|
||||||
|
readonly evaluator: ExpressionEvaluator;
|
||||||
|
readonly parameters?: readonly FunctionParameter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpressionFactory {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof Expression>
|
||||||
|
): IExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultRegexParserUtilities: RegexParserUtilities = {
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createPosition: createPositionFromRegexFullMatch,
|
||||||
|
createExpression: (...args) => new Expression(...args),
|
||||||
|
createParameterCollection: createFunctionParameterCollection,
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IPipe {
|
export interface Pipe {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
apply(input: string): string;
|
apply(input: string): string;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { IPipe } from '../IPipe';
|
import type { Pipe } from '../Pipe';
|
||||||
|
|
||||||
export class EscapeDoubleQuotes implements IPipe {
|
export class EscapeDoubleQuotes implements Pipe {
|
||||||
public readonly name: string = 'escapeDoubleQuotes';
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
|
|
||||||
public apply(raw: string): string {
|
public apply(raw: string): string {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return raw;
|
return '';
|
||||||
}
|
}
|
||||||
return raw.replaceAll('"', '"^""');
|
return raw.replaceAll('"', '"^""');
|
||||||
/* eslint-disable vue/max-len */
|
/* eslint-disable vue/max-len */
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IPipe } from '../IPipe';
|
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||||
|
import type { Pipe } from '../Pipe';
|
||||||
|
|
||||||
export class InlinePowerShell implements IPipe {
|
export class InlinePowerShell implements Pipe {
|
||||||
public readonly name: string = 'inlinePowerShell';
|
public readonly name: string = 'inlinePowerShell';
|
||||||
|
|
||||||
public apply(code: string): string {
|
public apply(code: string): string {
|
||||||
@@ -8,9 +9,11 @@ export class InlinePowerShell implements IPipe {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||||
|
// Order is important
|
||||||
inlineComments,
|
inlineComments,
|
||||||
mergeLinesWithBacktick,
|
|
||||||
mergeHereStrings,
|
mergeHereStrings,
|
||||||
|
mergeLinesWithBacktick,
|
||||||
|
mergeLinesWithBracketCodeBlocks,
|
||||||
mergeNewLines,
|
mergeNewLines,
|
||||||
]).reduce((a, b) => (data) => b(a(data)));
|
]).reduce((a, b) => (data) => b(a(data)));
|
||||||
const newCode = processor(code);
|
const newCode = processor(code);
|
||||||
@@ -89,10 +92,6 @@ function inlineComments(code: string): string {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLines(code: string): string[] {
|
|
||||||
return (code?.split(/\r\n|\r|\n/) || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
||||||
@@ -102,18 +101,18 @@ function mergeHereStrings(code: string) {
|
|||||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||||
const newString = getHereStringHandler(quotes);
|
const newString = getHereStringHandler(quotes);
|
||||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||||
const lines = getLines(escaped);
|
const lines = splitTextIntoLines(escaped);
|
||||||
const inlined = lines.join(newString.separator);
|
const inlined = lines.join(newString.separator);
|
||||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||||
return quoted;
|
return quoted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
interface IInlinedHereString {
|
interface InlinedHereString {
|
||||||
readonly quotesAround: string;
|
readonly quotesAround: string;
|
||||||
readonly escapedQuotes: string;
|
readonly escapedQuotes: string;
|
||||||
readonly separator: string;
|
readonly separator: string;
|
||||||
}
|
}
|
||||||
function getHereStringHandler(quotes: string): IInlinedHereString {
|
function getHereStringHandler(quotes: string): InlinedHereString {
|
||||||
/*
|
/*
|
||||||
We handle @' and @" differently.
|
We handle @' and @" differently.
|
||||||
Single quotes are interpreted literally and doubles are expandable.
|
Single quotes are interpreted literally and doubles are expandable.
|
||||||
@@ -158,9 +157,33 @@ function mergeLinesWithBacktick(code: string) {
|
|||||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeNewLines(code: string) {
|
/**
|
||||||
return getLines(code)
|
* Inlines code blocks in PowerShell scripts while preserving correct syntax.
|
||||||
.map((line) => line.trim())
|
* It removes unnecessary newlines and spaces around brackets,
|
||||||
.filter((line) => line.length > 0)
|
* inlining the code where possible.
|
||||||
.join('; ');
|
* This prevents syntax errors like "Unexpected token '}'" when inlining brackets.
|
||||||
|
*/
|
||||||
|
function mergeLinesWithBracketCodeBlocks(code: string): string {
|
||||||
|
return code
|
||||||
|
// Opening bracket: [whitespace] Opening bracket (newline)
|
||||||
|
.replace(/(?<=.*)\s*{[\r\n][\s\r\n]*/g, ' { ')
|
||||||
|
// Closing bracket: [whitespace] Closing bracket (newline) (continuation keyword)
|
||||||
|
.replace(/\s*}[\r\n][\s\r\n]*(?=elseif|else|catch|finally|until)/g, ' } ')
|
||||||
|
.replace(/(?<=do\s*{.*)[\r\n\s]*}[\r\n][\r\n\s]*(?=while)/g, ' } '); // Do-While
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeNewLines(code: string) {
|
||||||
|
const nonEmptyLines = splitTextIntoLines(code)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0);
|
||||||
|
|
||||||
|
return nonEmptyLines
|
||||||
|
.map((line, index) => {
|
||||||
|
const isLastLine = index === nonEmptyLines.length - 1;
|
||||||
|
if (isLastLine) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return line.endsWith(';') ? line : `${line};`;
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
import type { IPipe } from './IPipe';
|
import type { Pipe } from './Pipe';
|
||||||
|
|
||||||
const RegisteredPipes = [
|
const RegisteredPipes = [
|
||||||
new EscapeDoubleQuotes(),
|
new EscapeDoubleQuotes(),
|
||||||
@@ -8,19 +8,19 @@ const RegisteredPipes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export interface IPipeFactory {
|
export interface IPipeFactory {
|
||||||
get(pipeName: string): IPipe;
|
get(pipeName: string): Pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PipeFactory implements IPipeFactory {
|
export class PipeFactory implements IPipeFactory {
|
||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, Pipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
constructor(pipes: readonly Pipe[] = RegisteredPipes) {
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(pipeName: string): IPipe {
|
public get(pipeName: string): Pipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
const pipe = this.pipes.get(pipeName);
|
const pipe = this.pipes.get(pipeName);
|
||||||
if (!pipe) {
|
if (!pipe) {
|
||||||
@@ -29,7 +29,7 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
return pipe;
|
return pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: IPipe): void {
|
private registerPipe(pipe: Pipe): void {
|
||||||
validatePipeName(pipe.name);
|
validatePipeName(pipe.name);
|
||||||
if (this.pipes.has(pipe.name)) {
|
if (this.pipes.has(pipe.name)) {
|
||||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||||