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
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user