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:
@@ -463,7 +463,8 @@ actions:
|
||||
-
|
||||
name: Clear Internet Explorer DOMStore
|
||||
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:
|
||||
function: ClearDirectoryContents
|
||||
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],
|
||||
- `%APPDATA%\Mozilla\Firefox\Profiles\<profile folder>` on Windows 10 and later [1].
|
||||
|
||||
**Considerations**:
|
||||
- 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.
|
||||
- Close Firefox before running this script to prevent potential issues.
|
||||
> **Caution**:
|
||||
> - 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.
|
||||
> - 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"
|
||||
call:
|
||||
@@ -1204,7 +1205,7 @@ actions:
|
||||
(`%ProgramData%\Microsoft\Windows Defender\Scans\History\Service\DetectionHistory\[numbered folder]\`), and it contains a
|
||||
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
|
||||
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.
|
||||
@@ -1298,7 +1299,7 @@ actions:
|
||||
|
||||
**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.
|
||||
|
||||
[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
|
||||
recommend: standard
|
||||
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
|
||||
|
||||
`\Microsoft\Windows\Application Experience\ProgramDataUpdater`:
|
||||
@@ -1898,6 +1896,11 @@ actions:
|
||||
| ---------------- | -------------- |
|
||||
| Windows 10 22H2 | 🟢 Ready |
|
||||
| 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:
|
||||
function: DisableScheduledTask
|
||||
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
|
||||
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].
|
||||
|
||||
[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 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.
|
||||
|
||||
[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
|
||||
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:
|
||||
-
|
||||
name: Disable "Windows Defender Firewall Authorization Driver" service
|
||||
@@ -4985,32 +4988,38 @@ actions:
|
||||
fileGlob: '%SYSTEMROOT%\System32\drivers\mpsdrv.sys'
|
||||
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
||||
-
|
||||
name: Disable "Windows Defender Firewall" service
|
||||
docs:
|
||||
- http://batcmd.com/windows/10/services/mpssvc/
|
||||
- https://en.wikipedia.org/wiki/Windows_Firewall
|
||||
# More information about MpsSvc:
|
||||
- https://web.archive.org/web/20110203202612/http://technet.microsoft.com/en-us/library/dd364391(v=WS.10).aspx
|
||||
# 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
|
||||
# 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
|
||||
# ❗️ Breaks Microsoft Store
|
||||
# Can no longer update nor install apps, they both fail with 0x80073D0A
|
||||
# Also breaks some of Store apps such as Photos:
|
||||
# - 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
|
||||
# > The MpsSvc service host much more functionality than just windows firewall. For instance, Windows
|
||||
# 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
|
||||
# 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
|
||||
# anything special once you install a 3rd party security product.
|
||||
# Source: https://www.walkernews.net/2012/09/23/how-to-fix-windows-store-app-update-error-code-0x80073d0a/
|
||||
# ❗️ Breaks: `netsh advfirewall set`
|
||||
# Disabling and stopping it breaks "netsh advfirewall set" commands such as
|
||||
# `netsh advfirewall set allprofiles state on`, `netsh advfirewall set allprofiles state off`.
|
||||
# More about `netsh firewall` context: https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/netsh-advfirewall-firewall-control-firewall-behavior
|
||||
name: Disable "Windows Defender Firewall" service (breaks Microsoft Store downloads and `netsh advfirewall` CLI)
|
||||
docs: |-
|
||||
This script disables the "Windows Defender Firewall" service, also known as `MpsSvc` [1] [2] [3].
|
||||
|
||||
The Windows Defender Firewall, previously known as Windows Firewall [4], is a component that helps protect against unauthorized network access [3] [4].
|
||||
It operates by filtering both incoming and outgoing network traffic based on predefined security rules [1].
|
||||
|
||||
Disabling the Windows Defender Firewall has significant impacts, including:
|
||||
|
||||
- **Microsoft Store app downloads**: Disabling this service prevents updates and installations from the Microsoft Store, resulting in error code `0x80073D0A` [5] [6].
|
||||
- **`netsh advfirewall` commands**: The script renders the `netsh advfirewall` command-line context, which manages Windows Firewall settings [7], becomes inoperative.
|
||||
- **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].
|
||||
This feature was introduced to minimize vulnerabilities during startup [2].
|
||||
|
||||
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
|
||||
firewalls typically interact with Windows Firewall via public APIs, rather than disabling the service outright [6].
|
||||
|
||||
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].
|
||||
|
||||
> **Caution:** Disabling this service significantly compromises system security [9] and is not recommended by Microsoft [9].
|
||||
> 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.
|
||||
> Users should be aware of these considerable trade-offs when considering this script for privacy enhancement.
|
||||
|
||||
[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"
|
||||
[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"
|
||||
[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:
|
||||
-
|
||||
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"
|
||||
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
|
||||
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
|
||||
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
|
||||
privacy.
|
||||
|
||||
@@ -8804,7 +8813,7 @@ actions:
|
||||
packageName: Microsoft.Getstarted
|
||||
publisherId: 8wekyb3d8bbwe
|
||||
-
|
||||
category: Remove extensions
|
||||
category: Remove extension apps
|
||||
docs: |-
|
||||
This category focuses on scripts designed to uninstall specific extensions from Windows.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue';
|
||||
import { createRenderer } from './MarkdownRenderer';
|
||||
import { createMarkdownRenderer } from './MarkdownRenderer';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -27,7 +27,7 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
const renderer = createRenderer();
|
||||
const renderer = createMarkdownRenderer();
|
||||
|
||||
function renderText(docs: readonly string[] | undefined): string {
|
||||
if (!docs || docs.length === 0) {
|
||||
|
||||
@@ -2,85 +2,107 @@ import MarkdownIt from 'markdown-it';
|
||||
import Renderer from 'markdown-it/lib/renderer';
|
||||
import Token from 'markdown-it/lib/token';
|
||||
|
||||
export function createRenderer(): IRenderer {
|
||||
const md = new MarkdownIt({
|
||||
linkify: true, // Auto-convert URL-like text to links.
|
||||
breaks: false, // Do not convert single '\n's into <br>.
|
||||
export function createMarkdownRenderer(): MarkdownRenderer {
|
||||
const markdownParser = new MarkdownIt({
|
||||
linkify: false, // Disables auto-linking; handled manually for custom formatting.
|
||||
breaks: false, // Disables conversion of single newlines (`\n`) to HTML breaks (`<br>`).
|
||||
});
|
||||
openUrlsInNewTab(md);
|
||||
configureLinksToOpenInNewTab(markdownParser);
|
||||
return {
|
||||
render: (markdown: string) => {
|
||||
markdown = beatifyAutoLinks(markdown);
|
||||
return md.render(markdown);
|
||||
render: (markdownContent: string) => {
|
||||
markdownContent = beautifyAutoLinkedUrls(markdownContent);
|
||||
return markdownParser.render(markdownContent);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRenderer {
|
||||
render(markdown: string): string;
|
||||
interface MarkdownRenderer {
|
||||
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) {
|
||||
return content;
|
||||
}
|
||||
return content.replaceAll(/(?<!\]\(|\[\d+\]:\s+|https?\S+|`)((?:https?):\/\/[^\s\])]*)(?:[\])](?!\()|$|\s)/gm, (_$, urlMatch) => {
|
||||
return toReadableLink(urlMatch);
|
||||
return content.replaceAll(PlainTextUrlInMarkdownRegex, (_fullMatch, url) => {
|
||||
return formatReadableLink(url);
|
||||
});
|
||||
}
|
||||
|
||||
function toReadableLink(url: string): string {
|
||||
const parts = new URL(url);
|
||||
let displayName = toReadableHostName(parts.hostname);
|
||||
const pageName = extractPageName(parts);
|
||||
if (pageName) {
|
||||
displayName += ` - ${truncateRight(capitalizeEachLetter(pageName), 50)}`;
|
||||
function formatReadableLink(url: string): string {
|
||||
const urlParts = new URL(url);
|
||||
let displayText = formatHostName(urlParts.hostname);
|
||||
const pageLabel = extractPageLabel(urlParts);
|
||||
if (pageLabel) {
|
||||
displayText += ` - ${truncateTextFromEnd(capitalizeEachWord(pageLabel), 50)}`;
|
||||
}
|
||||
return `[${displayName}](${parts.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;
|
||||
if (displayText.includes('Msdn.microsoft')) {
|
||||
console.log(`[${displayText}](${urlParts.href})`);
|
||||
}
|
||||
return toReadableQuery(parts);
|
||||
return buildMarkdownLink(displayText, urlParts.href);
|
||||
}
|
||||
|
||||
function toReadableQuery(parts: URL): string | undefined {
|
||||
const queryValues = [...parts.searchParams.values()];
|
||||
function formatHostName(hostname: string): string {
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
return selectMostDescriptiveName(queryValues);
|
||||
return findMostDescriptiveName(queryValues);
|
||||
}
|
||||
|
||||
function truncateLeft(phrase: string, threshold: number): string {
|
||||
return phrase.length > threshold ? `…${phrase.substring(phrase.length - threshold, phrase.length)}` : phrase;
|
||||
function truncateTextFromStart(text: string, maxLength: number): string {
|
||||
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);
|
||||
}
|
||||
|
||||
function toReadablePath(path: string): string | undefined {
|
||||
const decodedPath = decodeURI(path); // Fixes e.g. %20 to whitespaces
|
||||
const pathPart = selectMostDescriptiveName(decodedPath.split('/'));
|
||||
if (!pathPart) {
|
||||
function makePathReadable(path: string): string | undefined {
|
||||
const decodedPath = decodeURI(path); // Decode URI components, e.g., '%20' to space
|
||||
const pathParts = decodedPath.split('/');
|
||||
const decodedPathParts = pathParts // Split then decode to correctly handle '%2F' as '/'
|
||||
.map((pathPart) => decodeURIComponent(pathPart));
|
||||
const descriptivePart = findMostDescriptiveName(decodedPathParts);
|
||||
if (!descriptivePart) {
|
||||
return undefined;
|
||||
}
|
||||
const extensionStripped = removeTrailingExtension(pathPart);
|
||||
const humanlyTokenized = extensionStripped.replaceAll(/[-_]/g, ' ');
|
||||
return humanlyTokenized;
|
||||
const withoutExtension = removeFileExtension(descriptivePart);
|
||||
const tokenizedText = tokenizeTextForReadability(withoutExtension);
|
||||
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('.');
|
||||
if (parts.length === 1) {
|
||||
return value;
|
||||
@@ -92,71 +114,68 @@ function removeTrailingExtension(value: string): string {
|
||||
return parts.slice(0, -1).join('.');
|
||||
}
|
||||
|
||||
function capitalizeEachLetter(phrase: string): string {
|
||||
return phrase
|
||||
function capitalizeEachWord(text: string): string {
|
||||
return text
|
||||
.split(' ')
|
||||
.map((word) => capitalizeFirstLetter(word))
|
||||
.join(' ');
|
||||
function capitalizeFirstLetter(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
function truncateRight(phrase: string, threshold: number): string {
|
||||
return phrase.length > threshold ? `${phrase.substring(0, threshold)}…` : phrase;
|
||||
function capitalizeFirstLetter(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
function selectMostDescriptiveName(parts: readonly string[]): string | undefined {
|
||||
const goodParts = parts.filter(isGoodPathPart);
|
||||
if (goodParts.length === 0) {
|
||||
function findMostDescriptiveName(segments: readonly string[]): string | undefined {
|
||||
const meaningfulSegments = segments.filter(isMeaningfulPathSegment);
|
||||
if (meaningfulSegments.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const longestGoodPart = goodParts.reduce((a, b) => (a.length > b.length ? a : b));
|
||||
return longestGoodPart;
|
||||
const longestGoodSegment = meaningfulSegments.reduce((a, b) => (a.length > b.length ? a : b));
|
||||
return longestGoodSegment;
|
||||
}
|
||||
|
||||
function isGoodPathPart(part: string): boolean {
|
||||
return part.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !isDigit(part) // E.g. article numbers, issue numbers
|
||||
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // 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
|
||||
&& !/^[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]{40}$/.test(part); // Git SHA (e.g. GitHub links)
|
||||
function isMeaningfulPathSegment(segment: string): boolean {
|
||||
return segment.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !isNumeric(segment) // E.g. article numbers, issue numbers
|
||||
&& !/^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(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(segment) // GUID
|
||||
&& !/^[0-9a-f]{40}$/.test(segment); // Git SHA (e.g. GitHub links)
|
||||
}
|
||||
|
||||
const ExternalAnchorElementAttributes: Record<string, string> = {
|
||||
const AnchorAttributesForExternalLinks: Record<string, string> = {
|
||||
target: '_blank',
|
||||
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
|
||||
const defaultRender = getOrDefaultRenderer(md, 'link_open');
|
||||
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
const defaultLinkRenderer = getDefaultRenderer(markdownParser, 'link_open');
|
||||
markdownParser.renderer.rules.link_open = (tokens, index, options, env, self) => {
|
||||
const currentToken = tokens[index];
|
||||
|
||||
Object.entries(ExternalAnchorElementAttributes).forEach(([name, value]) => {
|
||||
const currentValue = getAttribute(token, name);
|
||||
if (!currentValue) {
|
||||
token.attrPush([name, value]);
|
||||
} else if (currentValue !== value) {
|
||||
setAttribute(token, name, value);
|
||||
Object.entries(AnchorAttributesForExternalLinks).forEach(([attribute, value]) => {
|
||||
const existingValue = getTokenAttribute(currentToken, attribute);
|
||||
if (!existingValue) {
|
||||
addAttributeToToken(currentToken, attribute, value);
|
||||
} else if (existingValue !== 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 {
|
||||
const renderer = md.renderer.rules[ruleName];
|
||||
return renderer || defaultRenderer;
|
||||
function defaultRenderer(tokens, idx, options, _env, self) {
|
||||
function getDefaultRenderer(md: MarkdownIt, ruleName: string): Renderer.RenderRule {
|
||||
const ruleRenderer = md.renderer.rules[ruleName];
|
||||
return ruleRenderer || renderTokenAsDefault;
|
||||
function renderTokenAsDefault(tokens, idx, options, _env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
}
|
||||
|
||||
function getAttribute(token: Token, name: string): string | undefined {
|
||||
const attributeIndex = token.attrIndex(name);
|
||||
function getTokenAttribute(token: Token, attributeName: string): string | undefined {
|
||||
const attributeIndex = token.attrIndex(attributeName);
|
||||
if (attributeIndex < 0) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -164,10 +183,14 @@ function getAttribute(token: Token, name: string): string | undefined {
|
||||
return value;
|
||||
}
|
||||
|
||||
function setAttribute(token: Token, name: string, value: string): void {
|
||||
const attributeIndex = token.attrIndex(name);
|
||||
if (attributeIndex < 0) {
|
||||
throw new Error('Attribute does not exist');
|
||||
}
|
||||
token.attrs[attributeIndex][1] = value;
|
||||
function addAttributeToToken(token: Token, attributeName: string, value: string): void {
|
||||
token.attrPush([attributeName, value]);
|
||||
}
|
||||
|
||||
function updateTokenAttribute(token: Token, attributeName: string, newValue: string): void {
|
||||
const attributeIndex = token.attrIndex(attributeName);
|
||||
if (attributeIndex < 0) {
|
||||
throw new Error(`Attribute "${attributeName}" not found in token.`);
|
||||
}
|
||||
token.attrs[attributeIndex][1] = newValue;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseApplication } from '@/application/Parser/ApplicationParser';
|
||||
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('can render all docs', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
const renderer = createMarkdownRenderer();
|
||||
for (const node of collectAllDocumentableNodes()) {
|
||||
it(`${node.nodeLabel}`, () => {
|
||||
// act
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
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('createRenderer', () => {
|
||||
it('can create', () => {
|
||||
describe('createMarkdownRenderer', () => {
|
||||
it('creates renderer instance', () => {
|
||||
// arrange & act
|
||||
const renderer = createRenderer();
|
||||
const renderer = createMarkdownRenderer();
|
||||
// assert
|
||||
expect(renderer !== undefined);
|
||||
});
|
||||
describe('sets expected anchor attributes', () => {
|
||||
describe('sets default anchor attributes', () => {
|
||||
const attributes: ReadonlyArray<{
|
||||
readonly name: string,
|
||||
readonly attributeName: string,
|
||||
readonly expectedValue: string,
|
||||
readonly invalidMarkdown: string
|
||||
}> = [
|
||||
{
|
||||
name: 'target',
|
||||
attributeName: 'target',
|
||||
expectedValue: '_blank',
|
||||
invalidMarkdown: '<a href="https://undergroundwires.dev" target="_self">example</a>',
|
||||
},
|
||||
{
|
||||
name: 'rel',
|
||||
attributeName: 'rel',
|
||||
expectedValue: 'noopener noreferrer',
|
||||
invalidMarkdown: '<a href="https://undergroundwires.dev" rel="nooverride">example</a>',
|
||||
},
|
||||
];
|
||||
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
|
||||
const renderer = createRenderer();
|
||||
const renderer = createMarkdownRenderer();
|
||||
const markdown = '[undergroundwires.dev](https://undergroundwires.dev)';
|
||||
|
||||
// act
|
||||
@@ -40,12 +40,12 @@ describe('MarkdownRenderer', () => {
|
||||
// assert
|
||||
const html = parseHtml(htmlString);
|
||||
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
|
||||
const renderer = createRenderer();
|
||||
const renderer = createMarkdownRenderer();
|
||||
|
||||
// act
|
||||
const htmlString = renderer.render(invalidMarkdown);
|
||||
@@ -53,13 +53,13 @@ describe('MarkdownRenderer', () => {
|
||||
// assert
|
||||
const html = parseHtml(htmlString);
|
||||
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
|
||||
const renderer = createRenderer();
|
||||
const renderer = createMarkdownRenderer();
|
||||
const markdown = 'Text with\nSingle\nLinebreaks';
|
||||
// act
|
||||
const htmlString = renderer.render(markdown);
|
||||
@@ -68,9 +68,9 @@ describe('MarkdownRenderer', () => {
|
||||
const totalBrElements = html.getElementsByTagName('br').length;
|
||||
expect(totalBrElements).to.equal(0);
|
||||
});
|
||||
it('creates links for plain URL', () => {
|
||||
it('converts plain URLs into hyperlinks', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
const renderer = createMarkdownRenderer();
|
||||
const expectedUrl = 'https://privacy.sexy/';
|
||||
const markdown = `Visit ${expectedUrl} now!`;
|
||||
// act
|
||||
@@ -81,18 +81,93 @@ describe('MarkdownRenderer', () => {
|
||||
const href = aElement.getAttribute('href');
|
||||
expect(href).to.equal(expectedUrl);
|
||||
});
|
||||
it('it generates beautiful labels for auto-linkified URL', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
const url = 'https://privacy.sexy';
|
||||
const expectedText = 'privacy.sexy';
|
||||
const markdown = `Visit ${url} now!`;
|
||||
// act
|
||||
const htmlString = renderer.render(markdown);
|
||||
// assert
|
||||
const html = parseHtml(htmlString);
|
||||
const aElement = html.getElementsByTagName('a')[0];
|
||||
expect(aElement.text).to.equal(expectedText);
|
||||
describe('generates readable labels for automatically linked URLs', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly urlText: string;
|
||||
readonly expectedLabel: string;
|
||||
}> = [
|
||||
{
|
||||
description: 'displays only domain name for URLs without path or query',
|
||||
urlText: 'https://privacy.sexy',
|
||||
expectedLabel: 'privacy.sexy',
|
||||
},
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user