import * as plugins from '../plugins.js'; import type { IDocsWebsiteHeading } from './interfaces.js'; const htmlEntityMap: Record = { amp: '&', apos: "'", gt: '>', lt: '<', quot: '"', }; export const escapeHtml = (valueArg: unknown): string => { return String(valueArg ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); }; export const decodeBasicHtml = (valueArg: string): string => { return valueArg.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (_match, entity: string) => { if (entity[0] === '#') { const isHex = entity[1]?.toLowerCase() === 'x'; const parsed = Number.parseInt(entity.slice(isHex ? 2 : 1), isHex ? 16 : 10); return Number.isFinite(parsed) ? String.fromCodePoint(parsed) : _match; } return htmlEntityMap[entity.toLowerCase()] ?? _match; }); }; export const stripHtml = (valueArg: string): string => { return decodeBasicHtml(valueArg.replace(/<[^>]*>/g, ' ')).replace(/\s+/g, ' ').trim(); }; export const slugify = (valueArg: string): string => { const result = String(valueArg) .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); return result || 'page'; }; export const titleFromPath = (relativePathArg: string): string => { const basename = plugins.path.posix.basename(relativePathArg).replace(/\.[^.]+$/, ''); if (basename.toLowerCase() === 'readme' || basename.toLowerCase() === 'index') { return 'Introduction'; } return basename .replace(/[._-]+/g, ' ') .replace(/\b\w/g, character => character.toUpperCase()); }; export const sectionFromPath = (relativePathArg: string): string => { const segments = relativePathArg.split('/'); if (segments.length < 2) return 'Guides'; return segments[0] .replace(/[._-]+/g, ' ') .replace(/\b\w/g, character => character.toUpperCase()); }; export const isPathInside = (rootPathArg: string, candidatePathArg: string): boolean => { const relative = plugins.path.relative(rootPathArg, candidatePathArg); return relative === '' || (!relative.startsWith(`..${plugins.path.sep}`) && relative !== '..' && !plugins.path.isAbsolute(relative)); }; export const assertRealPathInside = async ( rootRealPathArg: string, candidatePathArg: string, labelArg: string, ): Promise => { const candidateRealPath = await plugins.fs.realpath(candidatePathArg); if (!isPathInside(rootRealPathArg, candidateRealPath)) { throw new Error(`${labelArg} resolves outside the docs directory: ${candidatePathArg}`); } return candidateRealPath; }; export const toPosixRelativePath = (rootPathArg: string, absolutePathArg: string): string => { return plugins.path.relative(rootPathArg, absolutePathArg).split(plugins.path.sep).join('/'); }; export const addHeadingIds = ( htmlArg: string, pageIdArg: string, ): { html: string; headings: IDocsWebsiteHeading[] } => { const headings: IDocsWebsiteHeading[] = []; const counts = new Map(); const html = htmlArg.replace( /]*)>([\s\S]*?)<\/h\1>/gi, (_match, levelValue: string, attributes: string, contents: string) => { const text = stripHtml(contents); const baseSlug = slugify(text); const count = counts.get(baseSlug) ?? 0; counts.set(baseSlug, count + 1); const id = `${pageIdArg}-${baseSlug}${count > 0 ? `-${count + 1}` : ''}`; const cleanAttributes = attributes.replace(/\s+id=(?:"[^"]*"|'[^']*')/i, ''); const level = Number(levelValue) as 2 | 3; headings.push({ id, text, level }); return `${contents}`; }, ); return { html, headings }; }; export const enhanceCodeBlocks = (htmlArg: string): string => { return htmlArg.replace( /
([\s\S]*?)<\/code><\/pre>/gi,
    (_match, language: string | undefined, code: string) => {
      const label = language ? escapeHtml(language) : 'code';
      const languageClass = language ? ` class="language-${escapeHtml(language)}"` : '';
      return `
${label}
${code}
`; }, ); }; export const normalizeRepositoryUrl = (valueArg: unknown): string | undefined => { if (typeof valueArg !== 'string' || !valueArg.trim()) return undefined; const value = valueArg.trim().replace(/^git\+/, '').replace(/\.git$/, ''); try { const parsed = new URL(value); if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return undefined; return parsed.toString().replace(/\/$/, ''); } catch { return undefined; } }; export const normalizeAccentColor = (valueArg: unknown): string => { return typeof valueArg === 'string' && /^#[0-9a-f]{6}$/i.test(valueArg) ? valueArg.toLowerCase() : '#0071e3'; }; export const toBase64 = (valueArg: string | Uint8Array): string => { return Buffer.from(valueArg).toString('base64'); };