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

@@ -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) {

View File

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