/** * Markdown parser with special handling for nostr identifiers */ import { get } from 'svelte/store'; import { ndkInstance } from '$lib/ndk'; import { nip19 } from 'nostr-tools'; // Regular expressions for nostr identifiers - process these first const NOSTR_NPUB_REGEX = /(?:nostr:)?(npub[a-zA-Z0-9]{59,60})/g; // Regular expressions for markdown elements const BLOCKQUOTE_REGEX = /^(?:>[ \t]*.+\n?(?:(?:>[ \t]*\n)*(?:>[ \t]*.+\n?))*)+/gm; const ORDERED_LIST_REGEX = /^(\d+)\.[ \t]+(.+)$/gm; const UNORDERED_LIST_REGEX = /^[-*][ \t]+(.+)$/gm; const BOLD_REGEX = /\*\*([^*]+)\*\*|\*([^*]+)\*/g; const ITALIC_REGEX = /_([^_]+)_/g; const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm; const HORIZONTAL_RULE_REGEX = /^(?:---|\*\*\*|___)$/gm; const CODE_BLOCK_REGEX = /```([^\n]*)\n([\s\S]*?)```/gm; const INLINE_CODE_REGEX = /`([^`\n]+)`/g; const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g; const IMAGE_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g; const HASHTAG_REGEX = /(?(); /** * Get user metadata for an npub */ async function getUserMetadata(npub: string): Promise<{name?: string, displayName?: string}> { if (npubCache.has(npub)) { return npubCache.get(npub)!; } const fallback = { name: `${npub.slice(0, 8)}...${npub.slice(-4)}` }; try { const ndk = get(ndkInstance); if (!ndk) { npubCache.set(npub, fallback); return fallback; } const decoded = nip19.decode(npub); if (decoded.type !== 'npub') { npubCache.set(npub, fallback); return fallback; } const user = ndk.getUser({ npub: npub }); if (!user) { npubCache.set(npub, fallback); return fallback; } try { const profile = await user.fetchProfile(); if (!profile) { npubCache.set(npub, fallback); return fallback; } const metadata = { name: profile.name || fallback.name, displayName: profile.displayName }; npubCache.set(npub, metadata); return metadata; } catch (e) { npubCache.set(npub, fallback); return fallback; } } catch (e) { npubCache.set(npub, fallback); return fallback; } } /** * Process lists (ordered and unordered) */ function processLists(html: string): string { const lines = html.split('\n'); let inList = false; let isOrdered = false; let currentList: string[] = []; const processed: string[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const orderedMatch = ORDERED_LIST_REGEX.exec(line); const unorderedMatch = UNORDERED_LIST_REGEX.exec(line); if (orderedMatch || unorderedMatch) { if (!inList) { inList = true; isOrdered = !!orderedMatch; currentList = []; } const content = orderedMatch ? orderedMatch[2] : unorderedMatch![1]; currentList.push(content); } else { if (inList) { const listType = isOrdered ? 'ol' : 'ul'; const listClass = isOrdered ? 'list-decimal' : 'list-disc'; processed.push(`<${listType} class="${listClass} pl-6 my-4 space-y-1">`); currentList.forEach(item => { processed.push(`
${cleanContent}` }); return id; }); // Restore blockquotes blockquotes.forEach(({id, content}) => { processedText = processedText.replace(id, content); }); return processedText; } /** * Process code blocks and inline code before any HTML escaping */ function processCode(text: string): string { const blocks: Array<{id: string, content: string}> = []; const inlineCodes: Array<{id: string, content: string}> = []; let processedText = text; // First, extract and save code blocks processedText = processedText.replace(CODE_BLOCK_REGEX, (match, lang, code) => { const id = `CODE_BLOCK_${blocks.length}`; blocks.push({ id, content: `
${escapeHtml(code)}`
});
return id;
});
// Then extract and save inline code
processedText = processedText.replace(INLINE_CODE_REGEX, (match, code) => {
const id = `INLINE_CODE_${inlineCodes.length}`;
inlineCodes.push({
id,
content: `${escapeHtml(code.trim())}`
});
return id;
});
// Now escape HTML in the remaining text
processedText = escapeHtml(processedText);
// Restore code blocks
blocks.forEach(({id, content}) => {
processedText = processedText.replace(escapeHtml(id), content);
});
// Restore inline code
inlineCodes.forEach(({id, content}) => {
processedText = processedText.replace(escapeHtml(id), content);
});
return processedText;
}
/**
* Process footnotes with minimal spacing
*/
function processFootnotes(text: string): { text: string, footnotes: Map');
html = html.replace(/\n/g, '
');
// Wrap content in paragraph if needed
if (!html.startsWith('<')) {
html = `
${html}
`; } return html; } /** * Escape HTML special characters to prevent XSS */ function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Escape special characters in a string for use in a regular expression */ function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }