import { processNostrIdentifiers } from "../nostrUtils"; /** * Normalizes a string for use as a d-tag by converting to lowercase, * replacing non-alphanumeric characters with dashes, and removing * leading/trailing dashes. */ function normalizeDTag(input: string): string { return input .toLowerCase() .replace(/[^\p{L}\p{N}]/gu, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } /** * Replaces wikilinks in the format [[target]] or [[target|display]] with * clickable links to the events page. */ function replaceWikilinks(html: string): string { // [[target page]] or [[target page|display text]] return html.replace( /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_match, target, label) => { const normalized = normalizeDTag(target.trim()); const display = (label || target).trim(); const url = `./events?d=${normalized}`; // Output as a clickable with the [[display]] format and matching link colors return `${display}`; }, ); } /** * Replaces AsciiDoctor-generated empty anchor tags with clickable wikilink-style tags. */ function replaceAsciiDocAnchors(html: string): string { return html.replace( /<\/a>/g, (_match, id) => { const normalized = normalizeDTag(id.trim()); const url = `./events?d=${normalized}`; return `${id}`; } ); } /** * Processes nostr addresses in HTML content, but skips addresses that are * already within hyperlink tags. */ async function processNostrAddresses(html: string): Promise { // Helper to check if a match is within an existing tag function isWithinLink(text: string, index: number): boolean { // Look backwards from the match position to find the nearest tag const before = text.slice(0, index); const lastOpenTag = before.lastIndexOf(""); // If we find an opening tag after the last closing tag, we're inside a link return lastOpenTag > lastCloseTag; } // Process nostr addresses that are not within existing links const nostrPattern = /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/g; let processedHtml = html; // Find all nostr addresses const matches = Array.from(processedHtml.matchAll(nostrPattern)); // Process them in reverse order to avoid index shifting issues for (let i = matches.length - 1; i >= 0; i--) { const match = matches[i]; const [fullMatch] = match; const matchIndex = match.index ?? 0; // Skip if already within a link if (isWithinLink(processedHtml, matchIndex)) { continue; } // Process the nostr identifier const processedMatch = await processNostrIdentifiers(fullMatch); // Replace the match in the HTML processedHtml = processedHtml.slice(0, matchIndex) + processedMatch + processedHtml.slice(matchIndex + fullMatch.length); } return processedHtml; } /** * Fixes AsciiDoctor stem blocks for MathJax rendering. * Joins split spans and wraps content in $$...$$ for block math. */ function fixStemBlocks(html: string): string { // Replace
$...$
// with
$$...$$
return html.replace( /
\s*
\s*\$<\/span>([\s\S]*?)\$<\/span>\s*<\/div>\s*<\/div>/g, (_match, mathContent) => { // Remove any extra tags inside mathContent const cleanMath = mathContent.replace(/<\/?span[^>]*>/g, "").trim(); return `
$$${cleanMath}$$
`; }, ); } /** * Post-processes asciidoctor HTML output to add wikilink and nostr address rendering. * This function should be called after asciidoctor.convert() to enhance the HTML output. */ export async function postProcessAsciidoctorHtml( html: string, ): Promise { if (!html) return html; try { console.log('HTML before replaceWikilinks:', html); // First process AsciiDoctor-generated anchors let processedHtml = replaceAsciiDocAnchors(html); // Then process wikilinks in [[...]] format (if any remain) processedHtml = replaceWikilinks(processedHtml); // Then process nostr addresses (but not those already in links) processedHtml = await processNostrAddresses(processedHtml); processedHtml = fixStemBlocks(processedHtml); // Fix math blocks for MathJax return processedHtml; } catch (error) { console.error("Error in postProcessAsciidoctorHtml:", error); return html; // Return original HTML if processing fails } }