// url-normalize.mjs — Consistent URL normalization for sitemap ↔ reference file matching. // Zero dependencies. Idempotent: normalizeUrl(normalizeUrl(x)) === normalizeUrl(x). /** * Normalize a learn.microsoft.com URL to a canonical form. * Rules applied in order: * 0. Canonicalise scheme — accept schema-less + http(s) citations, store https * 1. Strip trailing punctuation leaked from markdown * 2. Strip fragment (#anchor) * 3. Strip ?view= and other query params * 4. Remove /en-us/ locale prefix (store locale-free) * 5. Lowercase * @param {string} raw * @returns {string|null} normalized URL, or null if not a learn.microsoft.com URL */ export function normalizeUrl(raw) { if (!raw || typeof raw !== 'string') return null; if (!raw.includes('learn.microsoft.com')) return null; let url = raw; // 0. Canonicalise scheme — citations appear schema-less (table cells: bare // learn.microsoft.com/...) or as http; strip any leading scheme and force // https so a schema-less citation matches the same key as its https form // (and the https sitemap entries) instead of creating a duplicate entry. url = url.replace(/^https?:\/\//i, ''); url = 'https://' + url; // 1. Strip trailing punctuation that leaked from markdown context // (incl. backtick from inline-code citations: `learn.microsoft.com/...`) url = url.replace(/[.,;:!?'")}\]`]+$/, ''); // 2. Strip fragment const hashIdx = url.indexOf('#'); if (hashIdx !== -1) url = url.slice(0, hashIdx); // 3. Strip query parameters (?view=, ?tabs=, etc.) const qIdx = url.indexOf('?'); if (qIdx !== -1) url = url.slice(0, qIdx); // 4. Remove /en-us/ locale prefix — store locale-free for consistent matching url = url.replace('://learn.microsoft.com/en-us/', '://learn.microsoft.com/'); // 5. Strip trailing slash for consistency url = url.replace(/\/+$/, ''); // 6. Lowercase url = url.toLowerCase(); return url; } /** * Extract all learn.microsoft.com URLs from a text string. * Handles all citation formats found in reference files: * - Markdown links: [text](https://learn.microsoft.com/...) * - Bare URLs on their own line * - URL: prefix format * - Dash-bullet format * - Table cell format * - Schema-less citations: bare learn.microsoft.com/ (no https://) * The scheme is optional but a path (`/...`) after the domain is REQUIRED — this * captures schema-less citations-with-path while rejecting bare-domain prose * mentions ("(learn.microsoft.com, blogs...)") and JSON example strings * ({"url": "learn.microsoft.com"}), which carry no trackable source. * @param {string} text * @returns {string[]} array of normalized unique URLs */ export function extractUrls(text) { if (!text) return []; const regex = /(?:https?:\/\/)?learn\.microsoft\.com\/[^\s)"'<>\]|`]+/g; const seen = new Set(); const results = []; let match; while ((match = regex.exec(text)) !== null) { const normalized = normalizeUrl(match[0]); if (normalized && !seen.has(normalized)) { seen.add(normalized); results.push(normalized); } } return results; }