Fix incorrect URL rendering in documentation texts

This commit addresses incorrect URL rendering within documentation text
by improving auto-linkified URL labels, handling `+` symbols as spaces,
enhancing readability of encoded path segments and manually updating
some of the documetation.

Key improvements:

- Parse `+` as whitespace in URLs for accurate script labeling.
- Interpret multiple whitespaces as single for robustness.
- Decode path segments for clearer links.
- Refactor markdown renderer.
- Expand unit tests for comprehensive coverage.

Documentation has been updated to fix inline URL references and improve
linkification across several scripts, ensuring more readable and
user-friendly content.

Affected files and documentation sections have been adjusted
accordingly, including script and category names for consistency and
clarity.

Some of the script/category documentation changing fixing URL rendering
includes:

- 'Disable sending information to Customer Experience Improvement
  Program':
  - Fix reference URLs being inlined.
- 'Disable "Secure boot" button in "Windows Security"':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
- 'Clear Internet Explorer DOMStore':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
- 'Disable "Windows Defender Firewall" service':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
  - Convert YAML comments to markdown comments visible by users.
  - Add breaking behavior to script name, changing script name to.
- 'Disable Microsoft Defender Firewall services and drivers':
  - Remove information about breaking behavior to avoid duplication and
    be consistent with the documentation of the rest of the collections.
- Use consistent styling for warning texts starting with `Caution:`.
- Rename 'Remove extensions' category to 'Remove extension apps' for
  consistency with names of its sibling categories.
This commit is contained in:
undergroundwires
2023-11-27 05:17:58 +01:00
parent bcad357017
commit d328f08952
5 changed files with 274 additions and 167 deletions

View File

@@ -463,7 +463,8 @@ actions:
- -
name: Clear Internet Explorer DOMStore name: Clear Internet Explorer DOMStore
recommend: standard recommend: standard
docs: https://web.archive.org/web/20100416135352/http://msdn.microsoft.com/en-us/library/cc197062(VS.85).aspx docs: |-
[Introduction to DOM Storage | msdn.microsoft.com](https://web.archive.org/web/20100416135352/http://msdn.microsoft.com/en-us/library/cc197062(VS.85).aspx)
call: call:
function: ClearDirectoryContents function: ClearDirectoryContents
parameters: parameters:
@@ -593,10 +594,10 @@ actions:
- `C:\Documents and Settings\<Windows login/user name>\Application Data\Mozilla\Firefox\Profiles\<profile folder>` on Windows XP and earlier [1], - `C:\Documents and Settings\<Windows login/user name>\Application Data\Mozilla\Firefox\Profiles\<profile folder>` on Windows XP and earlier [1],
- `%APPDATA%\Mozilla\Firefox\Profiles\<profile folder>` on Windows 10 and later [1]. - `%APPDATA%\Mozilla\Firefox\Profiles\<profile folder>` on Windows 10 and later [1].
**Considerations**: > **Caution**:
- Using this script results in a total loss of all personalized Firefox data. > - Using this script results in a total loss of all personalized Firefox data.
- If your goal is solely to clear browsing data while retaining settings and extensions, this script is not recommended. > - If your goal is solely to clear browsing data while retaining settings and extensions, this script is not recommended.
- Close Firefox before running this script to prevent potential issues. > - Close Firefox before running this script to prevent potential issues.
[1]: https://web.archive.org/web/20231101125909/https://kb.mozillazine.org/Profile_folder_-_Firefox#Windows "Profile folder - Firefox - MozillaZine Knowledge Base | kb.mozillazine.org" [1]: https://web.archive.org/web/20231101125909/https://kb.mozillazine.org/Profile_folder_-_Firefox#Windows "Profile folder - Firefox - MozillaZine Knowledge Base | kb.mozillazine.org"
call: call:
@@ -1204,7 +1205,7 @@ actions:
(`%ProgramData%\Microsoft\Windows Defender\Scans\History\Service\DetectionHistory\[numbered folder]\`), and it contains a (`%ProgramData%\Microsoft\Windows Defender\Scans\History\Service\DetectionHistory\[numbered folder]\`), and it contains a
system-generated ID for the event [2]. system-generated ID for the event [2].
> **Caution**: Deleting these logs may decrease your security. These logs help in keeping track of potential threats and their sources, > **Caution:** Deleting these logs may decrease your security. These logs help in keeping track of potential threats and their sources,
allowing for a more proactive response in future encounters. Without this history, Microsoft Defender might not recognize recurring threats allowing for a more proactive response in future encounters. Without this history, Microsoft Defender might not recognize recurring threats
as quickly, possibly leaving your system more vulnerable. It's essential to understand that you're making a trade-off between enhanced as quickly, possibly leaving your system more vulnerable. It's essential to understand that you're making a trade-off between enhanced
privacy and potentially reduced security. privacy and potentially reduced security.
@@ -1298,7 +1299,7 @@ actions:
**Windows Component Store** contains all the files that are required to Windows features on demand [3]. **Windows Component Store** contains all the files that are required to Windows features on demand [3].
WARNING: Once the "Reset Base" operation is activated, you will not be able to uninstall previous updates. However, this > **Caution:** Once the "Reset Base" operation is activated, you will not be able to uninstall previous updates. However, this
small trade-off improves your privacy and control over system data. small trade-off improves your privacy and control over system data.
[1]: https://web.archive.org/web/20230806160623/https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/what-is-dism?view=windows-11 "DISM Overview | Microsoft Learn" [1]: https://web.archive.org/web/20230806160623/https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/what-is-dism?view=windows-11 "DISM Overview | Microsoft Learn"
@@ -1887,9 +1888,6 @@ actions:
name: Disable sending information to Customer Experience Improvement Program name: Disable sending information to Customer Experience Improvement Program
recommend: standard recommend: standard
docs: |- docs: |-
[Turn off the Windows Customer Experience program - gHacks Tech News](https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/)
[Permanently Disabling Windows Compatibility Telemetry - Microsoft Community](https://answers.microsoft.com/en-us/windows/forum/windows_10-performance/permanently-disabling-windows-compatibility/6bf71583-81b0-4a74-ae2e-8fd73305aad1)
### Overview of default task statuses ### Overview of default task statuses
`\Microsoft\Windows\Application Experience\ProgramDataUpdater`: `\Microsoft\Windows\Application Experience\ProgramDataUpdater`:
@@ -1898,6 +1896,11 @@ actions:
| ---------------- | -------------- | | ---------------- | -------------- |
| Windows 10 22H2 | 🟢 Ready | | Windows 10 22H2 | 🟢 Ready |
| Windows 11 22H2 | 🟡 N/A (missing) | | Windows 11 22H2 | 🟡 N/A (missing) |
### Additional documentation
- [Turn off the Windows Customer Experience program - gHacks Tech News](https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/)
- [Permanently Disabling Windows Compatibility Telemetry - Microsoft Community](https://answers.microsoft.com/en-us/windows/forum/windows_10-performance/permanently-disabling-windows-compatibility/6bf71583-81b0-4a74-ae2e-8fd73305aad1)
call: call:
function: DisableScheduledTask function: DisableScheduledTask
parameters: parameters:
@@ -2390,7 +2393,7 @@ actions:
Taking control of this service prevents Microsoft from activating peer-to-peer sharing, enhancing user privacy. It ensures your device doesn't share update data Taking control of this service prevents Microsoft from activating peer-to-peer sharing, enhancing user privacy. It ensures your device doesn't share update data
or fetch it from arbitrary peers. or fetch it from arbitrary peers.
> **Caution**: Disabling this service affects the functionality of Windows Store. It plays a role not just in Windows Updates but also in Microsoft Store app > **Caution:** Disabling this service affects the functionality of Windows Store. It plays a role not just in Windows Updates but also in Microsoft Store app
downloads, especially since Windows 11 [7]. There have been reported issues with some app downloads on Windows 10 [8]. downloads, especially since Windows 11 [7]. There have been reported issues with some app downloads on Windows 10 [8].
[1]: https://web.archive.org/web/20230914164204/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization "What is Delivery Optimization? - Windows Deployment | Microsoft Learn" [1]: https://web.archive.org/web/20230914164204/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization "What is Delivery Optimization? - Windows Deployment | Microsoft Learn"
@@ -3878,7 +3881,7 @@ actions:
| Windows 10 22H2 | 🟡 N/A (missing) | | Windows 10 22H2 | 🟡 N/A (missing) |
| Windows 11 22H2 | 🟡 N/A (missing) | | Windows 11 22H2 | 🟡 N/A (missing) |
> **CAUTION:** Consider that while disabling this task may lead to increased privacy, it could also impact license compliance and the overall functionality > **Caution:** Consider that while disabling this task may lead to increased privacy, it could also impact license compliance and the overall functionality
of Microsoft Office products in the long run. of Microsoft Office products in the long run.
[1]: https://web.archive.org/web/20231024130456/https://learn.microsoft.com/en-us/microsoft-365/troubleshoot/licensing/subscription-automatic-license-renew-fails "Microsoft 365 subscription automatic license renewal fails when heartbeatcache in wrong location - Microsoft 365 | Microsoft Learn | learn.microsoft.com" [1]: https://web.archive.org/web/20231024130456/https://learn.microsoft.com/en-us/microsoft-365/troubleshoot/licensing/subscription-automatic-license-renew-fails "Microsoft 365 subscription automatic license renewal fails when heartbeatcache in wrong location - Microsoft 365 | Microsoft Learn | learn.microsoft.com"
@@ -4959,7 +4962,7 @@ actions:
category: Disable Microsoft Defender firewall # Also known as Windows Firewall, Microsoft Defender Firewall category: Disable Microsoft Defender firewall # Also known as Windows Firewall, Microsoft Defender Firewall
children: children:
- -
category: Disable Microsoft Defender Firewall services and drivers (breaks Microsoft Store and `netsh advfirewall` CLI) category: Disable Microsoft Defender Firewall services and drivers
children: children:
- -
name: Disable "Windows Defender Firewall Authorization Driver" service name: Disable "Windows Defender Firewall Authorization Driver" service
@@ -4985,32 +4988,38 @@ actions:
fileGlob: '%SYSTEMROOT%\System32\drivers\mpsdrv.sys' fileGlob: '%SYSTEMROOT%\System32\drivers\mpsdrv.sys'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
- -
name: Disable "Windows Defender Firewall" service name: Disable "Windows Defender Firewall" service (breaks Microsoft Store downloads and `netsh advfirewall` CLI)
docs: docs: |-
- http://batcmd.com/windows/10/services/mpssvc/ This script disables the "Windows Defender Firewall" service, also known as `MpsSvc` [1] [2] [3].
- https://en.wikipedia.org/wiki/Windows_Firewall
# More information about MpsSvc: The Windows Defender Firewall, previously known as Windows Firewall [4], is a component that helps protect against unauthorized network access [3] [4].
- https://web.archive.org/web/20110203202612/http://technet.microsoft.com/en-us/library/dd364391(v=WS.10).aspx It operates by filtering both incoming and outgoing network traffic based on predefined security rules [1].
# More information about boot time protection and stopping the firewall service:
- https://web.archive.org/web/20110131034058/http://blogs.technet.com:80/b/networking/archive/2009/03/24/stopping-the-windows-authenticating-firewall-service-and-the-boot-time-policy.aspx Disabling the Windows Defender Firewall has significant impacts, including:
# Stopping the service associated with Windows Firewall is not supported by Microsoft:
- https://web.archive.org/web/20121106033255/http://technet.microsoft.com/en-us/library/cc753180.aspx - **Microsoft Store app downloads**: Disabling this service prevents updates and installations from the Microsoft Store, resulting in error code `0x80073D0A` [5] [6].
# ❗️ Breaks Microsoft Store - **`netsh advfirewall` commands**: The script renders the `netsh advfirewall` command-line context, which manages Windows Firewall settings [7], becomes inoperative.
# Can no longer update nor install apps, they both fail with 0x80073D0A - **Activation of boot-time filters**: Deactivating the service may trigger boot-time filters that protect the computer during startup or when the firewall service stops unexpectedly [2].
# Also breaks some of Store apps such as Photos: This feature was introduced to minimize vulnerabilities during startup [2].
# - https://answers.microsoft.com/en-us/windows/forum/all/microsoft-store-windows-defender-windows-firewall/f2f68cd7-64ec-4fe1-ade4-9d12cde057f9
# - https://github.com/undergroundwires/privacy.sexy/issues/104#issuecomment-962651791 Beyond firewall functionality, the MpsSvc service is integral to Windows Service hardening and network isolation [6], essential for Windows Store applications [6]. As a result, third-party
# > The MpsSvc service host much more functionality than just windows firewall. For instance, Windows firewalls typically interact with Windows Firewall via public APIs, rather than disabling the service outright [6].
# Service hardening which is a windows protection of system services. It also host network isolatio
# which is a crucial part of the confidence model for Windows Store based applications. 3rd party firewalls The `MpsSvc` service is set to start automatically by default [3] and runs the `%WINDIR%\System32\MPSSVC.dll` driver [3]. This file is also referred to as "Microsoft Protection Service" [8].
# know this fact and instead of disabling the firewall service they coordinate through public APIs with Windows
# Firewall so that they can have ownership of the firewall policies of the computer. Hence you do not have to do > **Caution:** Disabling this service significantly compromises system security [9] and is not recommended by Microsoft [9].
# anything special once you install a 3rd party security product. > It affects not only the firewall's protective capabilities but also the functionality of other Windows components like the Store [5] [6] and command-line utilities.
# Source: https://www.walkernews.net/2012/09/23/how-to-fix-windows-store-app-update-error-code-0x80073d0a/ > Users should be aware of these considerable trade-offs when considering this script for privacy enhancement.
# ❗️ Breaks: `netsh advfirewall set`
# Disabling and stopping it breaks "netsh advfirewall set" commands such as [1]: https://web.archive.org/web/20110203202612/http://technet.microsoft.com/en-us/library/dd364391(v=WS.10).aspx "Windows Firewall Service | technet.microsoft.com"
# `netsh advfirewall set allprofiles state on`, `netsh advfirewall set allprofiles state off`. [2]: https://web.archive.org/web/20110131034058/http://blogs.technet.com:80/b/networking/archive/2009/03/24/stopping-the-windows-authenticating-firewall-service-and-the-boot-time-policy.aspx "Stopping the Windows Authenticating Firewall Service and the boot time policy - Microsoft Enterprise Networking Team - Site Home - TechNet Blogs | blogs.technet.com"
# More about `netsh firewall` context: https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/netsh-advfirewall-firewall-control-firewall-behavior [3]: https://web.archive.org/web/20231122132143/https://batcmd.com/windows/10/services/mpssvc/ "Windows Defender Firewall - Windows 10 Service - batcmd.com | batcmd.com"
[4]: https://en.wikipedia.org/w/index.php?title=Windows_Firewall&oldid=1183396285 "Windows Firewall - Wikipedia | wikipedia.org"
[5]: https://github.com/undergroundwires/privacy.sexy/issues/104#issuecomment-962651791 "[BUG][help wanted]: Cannot enable Windows Defender · Issue #104 · undergroundwires/privacy.sexy | github.com/undergroundwires/privacy.sexy"
[6]: https://web.archive.org/web/20200620033533/https://www.walkernews.net/2012/09/23/how-to-fix-windows-store-app-update-error-code-0x80073d0a/ "How To Fix Windows Store App Update Error Code 0x80073D0A? Walker News | www.walkernews.net"
[7]: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/netsh-advfirewall-firewall-control-firewall-behavior "Use netsh advfirewall firewall context - Windows Server | Microsoft Learn | learn.microsoft.com"
[8]: https://web.archive.org/web/20231122132150/https://strontic.github.io/xcyclopedia/library/MPSSVC.dll-AA441F7C99AAACBA2538E90D7693637A.html "MPSSVC.dll | Microsoft Protection Service | STRONTIC | strontic.github.io"
[9]: https://web.archive.org/web/20121106033255/http://technet.microsoft.com/en-us/library/cc753180.aspx "Basic Firewall Policy Design | technet.microsoft.com"
call: call:
- -
function: DisableServiceInRegistry # We must disable it on registry level, "Access is denied" for sc config function: DisableServiceInRegistry # We must disable it on registry level, "Access is denied" for sc config
@@ -6475,7 +6484,7 @@ actions:
- -
name: Disable "Secure boot" button in "Windows Security" name: Disable "Secure boot" button in "Windows Security"
docs: |- docs: |-
[Hide the Secure boot area | admx.help](https://web.archive.org/web/20231013162210/https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefenderSecurityCenter::DeviceSecurity_HideSecureBoot [Hide the Secure boot area | admx.help](https://web.archive.org/web/20231013162210/https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefenderSecurityCenter::DeviceSecurity_HideSecureBoot)
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Device security" /v "HideSecureBoot" /t REG_DWORD /d "1" /f code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Device security" /v "HideSecureBoot" /t REG_DWORD /d "1" /f
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Device security" /v "HideSecureBoot" /f 2>nul revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Device security" /v "HideSecureBoot" /f 2>nul
- -
@@ -6973,7 +6982,7 @@ actions:
4. **Configurational integrity**: Updates have the capacity to change pre-configured settings without explicit user consent. This could 4. **Configurational integrity**: Updates have the capacity to change pre-configured settings without explicit user consent. This could
result in unintended alteration of your privacy settings, leaving you exposed until you realize the change. result in unintended alteration of your privacy settings, leaving you exposed until you realize the change.
**Security implications**: While controlling updates enhances your privacy, it can leave your system vulnerable to unpatched exploits. > **Caution**: While controlling updates enhances your privacy, it can leave your system vulnerable to unpatched exploits.
Ensure that you manually review and apply updates on a regular basis. You're essentially trading off some security for a heightened level of Ensure that you manually review and apply updates on a regular basis. You're essentially trading off some security for a heightened level of
privacy. privacy.
@@ -8804,7 +8813,7 @@ actions:
packageName: Microsoft.Getstarted packageName: Microsoft.Getstarted
publisherId: 8wekyb3d8bbwe publisherId: 8wekyb3d8bbwe
- -
category: Remove extensions category: Remove extension apps
docs: |- docs: |-
This category focuses on scripts designed to uninstall specific extensions from Windows. This category focuses on scripts designed to uninstall specific extensions from Windows.

View File

@@ -9,7 +9,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, computed } from 'vue'; import { defineComponent, PropType, computed } from 'vue';
import { createRenderer } from './MarkdownRenderer'; import { createMarkdownRenderer } from './MarkdownRenderer';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -27,7 +27,7 @@ export default defineComponent({
}, },
}); });
const renderer = createRenderer(); const renderer = createMarkdownRenderer();
function renderText(docs: readonly string[] | undefined): string { function renderText(docs: readonly string[] | undefined): string {
if (!docs || docs.length === 0) { if (!docs || docs.length === 0) {

View File

@@ -2,85 +2,107 @@ import MarkdownIt from 'markdown-it';
import Renderer from 'markdown-it/lib/renderer'; import Renderer from 'markdown-it/lib/renderer';
import Token from 'markdown-it/lib/token'; import Token from 'markdown-it/lib/token';
export function createRenderer(): IRenderer { export function createMarkdownRenderer(): MarkdownRenderer {
const md = new MarkdownIt({ const markdownParser = new MarkdownIt({
linkify: true, // Auto-convert URL-like text to links. linkify: false, // Disables auto-linking; handled manually for custom formatting.
breaks: false, // Do not convert single '\n's into <br>. breaks: false, // Disables conversion of single newlines (`\n`) to HTML breaks (`<br>`).
}); });
openUrlsInNewTab(md); configureLinksToOpenInNewTab(markdownParser);
return { return {
render: (markdown: string) => { render: (markdownContent: string) => {
markdown = beatifyAutoLinks(markdown); markdownContent = beautifyAutoLinkedUrls(markdownContent);
return md.render(markdown); return markdownParser.render(markdownContent);
}, },
}; };
} }
export interface IRenderer { interface MarkdownRenderer {
render(markdown: string): string; render(markdownContent: string): string;
} }
function beatifyAutoLinks(content: string): string { const PlainTextUrlInMarkdownRegex = /(?<!\]\(|\[\d+\]:\s+|https?\S+|`)((?:https?):\/\/[^\s\])]*)(?:[\])](?!\()|$|\s)/gm;
function beautifyAutoLinkedUrls(content: string): string {
if (!content) { if (!content) {
return content; return content;
} }
return content.replaceAll(/(?<!\]\(|\[\d+\]:\s+|https?\S+|`)((?:https?):\/\/[^\s\])]*)(?:[\])](?!\()|$|\s)/gm, (_$, urlMatch) => { return content.replaceAll(PlainTextUrlInMarkdownRegex, (_fullMatch, url) => {
return toReadableLink(urlMatch); return formatReadableLink(url);
}); });
} }
function toReadableLink(url: string): string { function formatReadableLink(url: string): string {
const parts = new URL(url); const urlParts = new URL(url);
let displayName = toReadableHostName(parts.hostname); let displayText = formatHostName(urlParts.hostname);
const pageName = extractPageName(parts); const pageLabel = extractPageLabel(urlParts);
if (pageName) { if (pageLabel) {
displayName += ` - ${truncateRight(capitalizeEachLetter(pageName), 50)}`; displayText += ` - ${truncateTextFromEnd(capitalizeEachWord(pageLabel), 50)}`;
} }
return `[${displayName}](${parts.href})`; if (displayText.includes('Msdn.microsoft')) {
} console.log(`[${displayText}](${urlParts.href})`);
function toReadableHostName(hostname: string): string {
const wwwStripped = hostname.replace(/^(www\.)/, '');
const truncated = truncateLeft(wwwStripped, 30);
return truncated;
}
function extractPageName(parts: URL): string | undefined {
const path = toReadablePath(parts.pathname);
if (path) {
return path;
} }
return toReadableQuery(parts); return buildMarkdownLink(displayText, urlParts.href);
} }
function toReadableQuery(parts: URL): string | undefined { function formatHostName(hostname: string): string {
const queryValues = [...parts.searchParams.values()]; const withoutWww = hostname.replace(/^(www\.)/, '');
const truncatedHostName = truncateTextFromStart(withoutWww, 30);
return truncatedHostName;
}
function extractPageLabel(urlParts: URL): string | undefined {
const readablePath = makePathReadable(urlParts.pathname);
if (readablePath) {
return readablePath;
}
return formatQueryParameters(urlParts.searchParams);
}
function buildMarkdownLink(label: string, url: string): string {
return `[${label}](${url})`;
}
function formatQueryParameters(queryParameters: URLSearchParams): string | undefined {
const queryValues = [...queryParameters.values()];
if (queryValues.length === 0) { if (queryValues.length === 0) {
return undefined; return undefined;
} }
return selectMostDescriptiveName(queryValues); return findMostDescriptiveName(queryValues);
} }
function truncateLeft(phrase: string, threshold: number): string { function truncateTextFromStart(text: string, maxLength: number): string {
return phrase.length > threshold ? `${phrase.substring(phrase.length - threshold, phrase.length)}` : phrase; return text.length > maxLength ? `${text.substring(text.length - maxLength)}` : text;
} }
function isDigit(value: string): boolean { function truncateTextFromEnd(text: string, maxLength: number): string {
return text.length > maxLength ? `${text.substring(0, maxLength)}` : text;
}
function isNumeric(value: string): boolean {
return /^\d+$/.test(value); return /^\d+$/.test(value);
} }
function toReadablePath(path: string): string | undefined { function makePathReadable(path: string): string | undefined {
const decodedPath = decodeURI(path); // Fixes e.g. %20 to whitespaces const decodedPath = decodeURI(path); // Decode URI components, e.g., '%20' to space
const pathPart = selectMostDescriptiveName(decodedPath.split('/')); const pathParts = decodedPath.split('/');
if (!pathPart) { const decodedPathParts = pathParts // Split then decode to correctly handle '%2F' as '/'
.map((pathPart) => decodeURIComponent(pathPart));
const descriptivePart = findMostDescriptiveName(decodedPathParts);
if (!descriptivePart) {
return undefined; return undefined;
} }
const extensionStripped = removeTrailingExtension(pathPart); const withoutExtension = removeFileExtension(descriptivePart);
const humanlyTokenized = extensionStripped.replaceAll(/[-_]/g, ' '); const tokenizedText = tokenizeTextForReadability(withoutExtension);
return humanlyTokenized; return tokenizedText;
} }
function removeTrailingExtension(value: string): string { function tokenizeTextForReadability(text: string): string {
return text
.replaceAll(/[-_+]/g, ' ') // Replace hyphens, underscores, and plus signs with spaces
.replaceAll(/\s+/g, ' '); // Collapse multiple consecutive spaces into a single space
}
function removeFileExtension(value: string): string {
const parts = value.split('.'); const parts = value.split('.');
if (parts.length === 1) { if (parts.length === 1) {
return value; return value;
@@ -92,71 +114,68 @@ function removeTrailingExtension(value: string): string {
return parts.slice(0, -1).join('.'); return parts.slice(0, -1).join('.');
} }
function capitalizeEachLetter(phrase: string): string { function capitalizeEachWord(text: string): string {
return phrase return text
.split(' ') .split(' ')
.map((word) => capitalizeFirstLetter(word)) .map((word) => capitalizeFirstLetter(word))
.join(' '); .join(' ');
function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
} }
function truncateRight(phrase: string, threshold: number): string { function capitalizeFirstLetter(text: string): string {
return phrase.length > threshold ? `${phrase.substring(0, threshold)}` : phrase; return text.charAt(0).toUpperCase() + text.slice(1);
} }
function selectMostDescriptiveName(parts: readonly string[]): string | undefined { function findMostDescriptiveName(segments: readonly string[]): string | undefined {
const goodParts = parts.filter(isGoodPathPart); const meaningfulSegments = segments.filter(isMeaningfulPathSegment);
if (goodParts.length === 0) { if (meaningfulSegments.length === 0) {
return undefined; return undefined;
} }
const longestGoodPart = goodParts.reduce((a, b) => (a.length > b.length ? a : b)); const longestGoodSegment = meaningfulSegments.reduce((a, b) => (a.length > b.length ? a : b));
return longestGoodPart; return longestGoodSegment;
} }
function isGoodPathPart(part: string): boolean { function isMeaningfulPathSegment(segment: string): boolean {
return part.length > 2 // This is often non-human categories like T5 etc. return segment.length > 2 // This is often non-human categories like T5 etc.
&& !isDigit(part) // E.g. article numbers, issue numbers && !isNumeric(segment) // E.g. article numbers, issue numbers
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html && !/^index(?:\.\S{0,10}$|$)/.test(segment) // E.g. index.html
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR && !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(segment) // Locale string e.g. fr-FR
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(segment) // GUID
&& !/^[0-9a-f]{40}$/.test(part); // Git SHA (e.g. GitHub links) && !/^[0-9a-f]{40}$/.test(segment); // Git SHA (e.g. GitHub links)
} }
const ExternalAnchorElementAttributes: Record<string, string> = { const AnchorAttributesForExternalLinks: Record<string, string> = {
target: '_blank', target: '_blank',
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
}; };
function openUrlsInNewTab(md: MarkdownIt) { function configureLinksToOpenInNewTab(markdownParser: MarkdownIt): void {
// https://github.com/markdown-it/markdown-it/blob/12.2.0/docs/architecture.md#renderer // https://github.com/markdown-it/markdown-it/blob/12.2.0/docs/architecture.md#renderer
const defaultRender = getOrDefaultRenderer(md, 'link_open'); const defaultLinkRenderer = getDefaultRenderer(markdownParser, 'link_open');
md.renderer.rules.link_open = (tokens, idx, options, env, self) => { markdownParser.renderer.rules.link_open = (tokens, index, options, env, self) => {
const token = tokens[idx]; const currentToken = tokens[index];
Object.entries(ExternalAnchorElementAttributes).forEach(([name, value]) => { Object.entries(AnchorAttributesForExternalLinks).forEach(([attribute, value]) => {
const currentValue = getAttribute(token, name); const existingValue = getTokenAttribute(currentToken, attribute);
if (!currentValue) { if (!existingValue) {
token.attrPush([name, value]); addAttributeToToken(currentToken, attribute, value);
} else if (currentValue !== value) { } else if (existingValue !== value) {
setAttribute(token, name, value); updateTokenAttribute(currentToken, attribute, value);
} }
}); });
return defaultRender(tokens, idx, options, env, self); return defaultLinkRenderer(tokens, index, options, env, self);
}; };
} }
function getOrDefaultRenderer(md: MarkdownIt, ruleName: string): Renderer.RenderRule { function getDefaultRenderer(md: MarkdownIt, ruleName: string): Renderer.RenderRule {
const renderer = md.renderer.rules[ruleName]; const ruleRenderer = md.renderer.rules[ruleName];
return renderer || defaultRenderer; return ruleRenderer || renderTokenAsDefault;
function defaultRenderer(tokens, idx, options, _env, self) { function renderTokenAsDefault(tokens, idx, options, _env, self) {
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options);
} }
} }
function getAttribute(token: Token, name: string): string | undefined { function getTokenAttribute(token: Token, attributeName: string): string | undefined {
const attributeIndex = token.attrIndex(name); const attributeIndex = token.attrIndex(attributeName);
if (attributeIndex < 0) { if (attributeIndex < 0) {
return undefined; return undefined;
} }
@@ -164,10 +183,14 @@ function getAttribute(token: Token, name: string): string | undefined {
return value; return value;
} }
function setAttribute(token: Token, name: string, value: string): void { function addAttributeToToken(token: Token, attributeName: string, value: string): void {
const attributeIndex = token.attrIndex(name); token.attrPush([attributeName, value]);
if (attributeIndex < 0) { }
throw new Error('Attribute does not exist');
} function updateTokenAttribute(token: Token, attributeName: string, newValue: string): void {
token.attrs[attributeIndex][1] = value; const attributeIndex = token.attrIndex(attributeName);
if (attributeIndex < 0) {
throw new Error(`Attribute "${attributeName}" not found in token.`);
}
token.attrs[attributeIndex][1] = newValue;
} }

View File

@@ -1,12 +1,12 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { parseApplication } from '@/application/Parser/ApplicationParser'; import { parseApplication } from '@/application/Parser/ApplicationParser';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { createRenderer } from '@/presentation/components/Scripts/View/Tree/NodeContent/Documentation/MarkdownRenderer'; import { createMarkdownRenderer } from '@/presentation/components/Scripts/View/Tree/NodeContent/Documentation/MarkdownRenderer';
describe('MarkdownRenderer', () => { describe('MarkdownRenderer', () => {
describe('can render all docs', () => { describe('can render all docs', () => {
// arrange // arrange
const renderer = createRenderer(); const renderer = createMarkdownRenderer();
for (const node of collectAllDocumentableNodes()) { for (const node of collectAllDocumentableNodes()) {
it(`${node.nodeLabel}`, () => { it(`${node.nodeLabel}`, () => {
// act // act

View File

@@ -1,37 +1,37 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { createRenderer } from '@/presentation/components/Scripts/View/Tree/NodeContent/Documentation/MarkdownRenderer'; import { createMarkdownRenderer } from '@/presentation/components/Scripts/View/Tree/NodeContent/Documentation/MarkdownRenderer';
describe('MarkdownRenderer', () => { describe('MarkdownRenderer', () => {
describe('createRenderer', () => { describe('createMarkdownRenderer', () => {
it('can create', () => { it('creates renderer instance', () => {
// arrange & act // arrange & act
const renderer = createRenderer(); const renderer = createMarkdownRenderer();
// assert // assert
expect(renderer !== undefined); expect(renderer !== undefined);
}); });
describe('sets expected anchor attributes', () => { describe('sets default anchor attributes', () => {
const attributes: ReadonlyArray<{ const attributes: ReadonlyArray<{
readonly name: string, readonly attributeName: string,
readonly expectedValue: string, readonly expectedValue: string,
readonly invalidMarkdown: string readonly invalidMarkdown: string
}> = [ }> = [
{ {
name: 'target', attributeName: 'target',
expectedValue: '_blank', expectedValue: '_blank',
invalidMarkdown: '<a href="https://undergroundwires.dev" target="_self">example</a>', invalidMarkdown: '<a href="https://undergroundwires.dev" target="_self">example</a>',
}, },
{ {
name: 'rel', attributeName: 'rel',
expectedValue: 'noopener noreferrer', expectedValue: 'noopener noreferrer',
invalidMarkdown: '<a href="https://undergroundwires.dev" rel="nooverride">example</a>', invalidMarkdown: '<a href="https://undergroundwires.dev" rel="nooverride">example</a>',
}, },
]; ];
for (const attribute of attributes) { for (const attribute of attributes) {
const { name, expectedValue, invalidMarkdown } = attribute; const { attributeName, expectedValue, invalidMarkdown } = attribute;
it(`adds "${name}" attribute to anchor elements`, () => { it(`adds "${attributeName}" attribute to anchors`, () => {
// arrange // arrange
const renderer = createRenderer(); const renderer = createMarkdownRenderer();
const markdown = '[undergroundwires.dev](https://undergroundwires.dev)'; const markdown = '[undergroundwires.dev](https://undergroundwires.dev)';
// act // act
@@ -40,12 +40,12 @@ describe('MarkdownRenderer', () => {
// assert // assert
const html = parseHtml(htmlString); const html = parseHtml(htmlString);
const aElement = html.getElementsByTagName('a')[0]; const aElement = html.getElementsByTagName('a')[0];
expect(aElement.getAttribute(name)).to.equal(expectedValue); expect(aElement.getAttribute(attributeName)).to.equal(expectedValue);
}); });
it(`overrides existing "${name}" attribute`, () => { it(`overrides existing "${attributeName}" attribute`, () => {
// arrange // arrange
const renderer = createRenderer(); const renderer = createMarkdownRenderer();
// act // act
const htmlString = renderer.render(invalidMarkdown); const htmlString = renderer.render(invalidMarkdown);
@@ -53,13 +53,13 @@ describe('MarkdownRenderer', () => {
// assert // assert
const html = parseHtml(htmlString); const html = parseHtml(htmlString);
const aElement = html.getElementsByTagName('a')[0]; const aElement = html.getElementsByTagName('a')[0];
expect(aElement.getAttribute(name)).to.equal(expectedValue); expect(aElement.getAttribute(attributeName)).to.equal(expectedValue);
}); });
} }
}); });
it('does not convert single linebreak to <br>', () => { it('does not convert single line breaks to <br> elements', () => {
// arrange // arrange
const renderer = createRenderer(); const renderer = createMarkdownRenderer();
const markdown = 'Text with\nSingle\nLinebreaks'; const markdown = 'Text with\nSingle\nLinebreaks';
// act // act
const htmlString = renderer.render(markdown); const htmlString = renderer.render(markdown);
@@ -68,9 +68,9 @@ describe('MarkdownRenderer', () => {
const totalBrElements = html.getElementsByTagName('br').length; const totalBrElements = html.getElementsByTagName('br').length;
expect(totalBrElements).to.equal(0); expect(totalBrElements).to.equal(0);
}); });
it('creates links for plain URL', () => { it('converts plain URLs into hyperlinks', () => {
// arrange // arrange
const renderer = createRenderer(); const renderer = createMarkdownRenderer();
const expectedUrl = 'https://privacy.sexy/'; const expectedUrl = 'https://privacy.sexy/';
const markdown = `Visit ${expectedUrl} now!`; const markdown = `Visit ${expectedUrl} now!`;
// act // act
@@ -81,18 +81,93 @@ describe('MarkdownRenderer', () => {
const href = aElement.getAttribute('href'); const href = aElement.getAttribute('href');
expect(href).to.equal(expectedUrl); expect(href).to.equal(expectedUrl);
}); });
it('it generates beautiful labels for auto-linkified URL', () => { describe('generates readable labels for automatically linked URLs', () => {
// arrange const testScenarios: ReadonlyArray<{
const renderer = createRenderer(); readonly description: string;
const url = 'https://privacy.sexy'; readonly urlText: string;
const expectedText = 'privacy.sexy'; readonly expectedLabel: string;
const markdown = `Visit ${url} now!`; }> = [
// act {
const htmlString = renderer.render(markdown); description: 'displays only domain name for URLs without path or query',
// assert urlText: 'https://privacy.sexy',
const html = parseHtml(htmlString); expectedLabel: 'privacy.sexy',
const aElement = html.getElementsByTagName('a')[0]; },
expect(aElement.text).to.equal(expectedText); {
description: 'includes subdomains in labels',
urlText: 'https://subdomain.privacy.sexy',
expectedLabel: 'subdomain.privacy.sexy',
},
{
description: 'includes longer URL segment in label',
urlText: 'https://privacy.sexy/LongerExpectedPart/ShorterPart',
expectedLabel: 'privacy.sexy - LongerExpectedPart',
},
{
description: 'capitalizes first letter of URL path in label',
urlText: 'https://privacy.sexy/capitalized',
expectedLabel: 'privacy.sexy - Capitalized',
},
...['-', '%20', '+'].map((urlSegmentDelimiter) => ({
description: `parses \`${urlSegmentDelimiter}\` as a delimiter in URL`,
urlText: `https://privacy.sexy/privacy${urlSegmentDelimiter}scripts`,
expectedLabel: 'privacy.sexy - Privacy Scripts',
})),
{
description: 'treats multiple spaces as single in URLs',
urlText: 'https://privacy.sexy/word--with-multiple---spaces',
expectedLabel: 'privacy.sexy - Word With Multiple Spaces',
},
{
description: 'handles query parameters in URLs correctly',
urlText: 'https://privacy.sexy/?query=parameter',
expectedLabel: 'privacy.sexy - Parameter',
},
{
description: 'truncates long hostnames to a readable length',
urlText: 'https://averylongwebsitedomainnamethatexceedsthetypicalthreshold.com',
expectedLabel: '…exceedsthetypicalthreshold.com',
},
{
description: 'ignores purely numeric path segments in labels',
urlText: 'https://privacy.sexy/20230302/expected',
expectedLabel: 'privacy.sexy - Expected',
},
{
description: 'ignores non-standard ports in labels',
urlText: 'https://privacy.sexy:8080/configure',
expectedLabel: 'privacy.sexy - Configure',
},
{
description: 'removes common file extensions from labels',
urlText: 'https://privacy.sexy/image.png',
expectedLabel: 'privacy.sexy - Image',
},
{
description: 'handles complex queries in URLs by selecting the most descriptive part',
urlText: 'https://privacy.sexy/?product=123&name=PrivacyTool',
expectedLabel: 'privacy.sexy - PrivacyTool',
},
{
description: 'decodes special encoded characters in URLs for labels',
urlText: 'https://privacy.sexy/privacy%2Fscripts',
expectedLabel: 'privacy.sexy - Privacy/scripts',
},
];
testScenarios.forEach(({
description, urlText, expectedLabel,
}) => {
it(description, () => {
// arrange
const renderer = createMarkdownRenderer();
const markdown = `Visit ${urlText} now!`;
// act
const htmlString = renderer.render(markdown);
// assert
const html = parseHtml(htmlString);
const aElement = html.getElementsByTagName('a')[0];
expect(aElement.text).to.equal(expectedLabel);
});
});
}); });
}); });
}); });