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