From 134ce9a52d8922a8ad6227cee1d55d69fd062f9a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 23 Aug 2025 21:24:55 -0500 Subject: [PATCH] Reorganize markup processing code to reduce duplication --- .../utils/markup/asciidoctorPostProcessor.ts | 49 +--- src/lib/utils/markup/basicMarkupParser.ts | 232 ++--------------- src/lib/utils/markup/embeddedMarkupParser.ts | 237 +----------------- .../{markupServices.ts => markupUtils.ts} | 202 +++++++++++++++ 4 files changed, 232 insertions(+), 488 deletions(-) rename src/lib/utils/markup/{markupServices.ts => markupUtils.ts} (70%) diff --git a/src/lib/utils/markup/asciidoctorPostProcessor.ts b/src/lib/utils/markup/asciidoctorPostProcessor.ts index 8eb84a3..090ae94 100644 --- a/src/lib/utils/markup/asciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/asciidoctorPostProcessor.ts @@ -3,54 +3,7 @@ import { processImageWithReveal, processNostrIdentifiersInText, processWikilinks, -} from "./markupServices.ts"; - -/** - * 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 processNostrIdentifiersInText(fullMatch); - - // Replace the match in the HTML - processedHtml = processedHtml.slice(0, matchIndex) + - processedMatch + - processedHtml.slice(matchIndex + fullMatch.length); - } - - return processedHtml; -} +} from "./markupUtils.ts"; /** * Processes AsciiDoc image blocks to add reveal/enlarge functionality diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index 6c7611f..50a8a15 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -1,233 +1,50 @@ -import { nip19 } from "nostr-tools"; import NDK from "@nostr-dev-kit/ndk"; import { - processBasicTextFormatting, +processBasicFormatting, processBlockquotes, processEmojiShortcodes, - processHashtags, - processImageWithReveal, - processMediaUrl, processNostrIdentifiersInText, - processWebSocketUrls, processWikilinks, - stripTrackingParams, -} from "./markupServices.ts"; +} from "./markupUtils.ts"; -/* Regex constants for basic markup parsing */ -// Links and media -const MARKUP_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; -const MARKUP_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; -// AI-NOTE: 2025-01-24 - Added negative lookbehind (?"]+)(?!["'])/g; -// Add this helper function near the top: -function replaceAlexandriaNostrLinks(text: string): string { - // Regex for Alexandria/localhost URLs - const alexandriaPattern = - /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i; - // Regex for bech32 Nostr identifiers - const bech32Pattern = /(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/; - // Regex for 64-char hex - const hexPattern = /\b[a-fA-F0-9]{64}\b/; - - // 1. Alexandria/localhost markup links - text = text.replace( - /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, - (match, _label, url) => { - if (alexandriaPattern.test(url)) { - if (/[?&]d=/.test(url)) return match; - const hexMatch = url.match(hexPattern); - if (hexMatch) { - try { - const nevent = nip19.neventEncode({ id: hexMatch[0] }); - return `nostr:${nevent}`; - } catch { - return match; - } - } - const bech32Match = url.match(bech32Pattern); - if (bech32Match) { - return `nostr:${bech32Match[0]}`; - } - } - return match; - }, - ); +export function preProcessBasicMarkup(text: string): string { + try { + // Process basic text formatting first + let processedText = processBasicFormatting(text); - // 2. Alexandria/localhost bare URLs and non-Alexandria/localhost URLs with Nostr identifiers - text = text.replace(/https?:\/\/[^\s)\]]+/g, (url) => { - if (alexandriaPattern.test(url)) { - if (/[?&]d=/.test(url)) return url; - const hexMatch = url.match(hexPattern); - if (hexMatch) { - try { - const nevent = nip19.neventEncode({ id: hexMatch[0] }); - return `nostr:${nevent}`; - } catch { - return url; - } - } - const bech32Match = url.match(bech32Pattern); - if (bech32Match) { - return `nostr:${bech32Match[0]}`; - } - } - // For non-Alexandria/localhost URLs, just return the URL as-is - return url; - }); + // Process emoji shortcuts + processedText = processEmojiShortcodes(processedText); - return text; -} + // Process blockquotes + processedText = processBlockquotes(processedText); -function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { - function parseList( - start: number, - indent: number, - type: "ol" | "ul", - ): [string, number] { - let html = ""; - let i = start; - html += `<${type} class="${ - type === "ol" ? "list-decimal" : "list-disc" - } ml-6 mb-2">`; - while (i < lines.length) { - const line = lines[i]; - const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/); - if (!match) break; - const lineIndent = match[1].replace(/\t/g, " ").length; - const isOrdered = /\d+\./.test(match[2]); - const itemType = isOrdered ? "ol" : "ul"; - if (lineIndent > indent) { - // Nested list - const [nestedHtml, consumed] = parseList(i, lineIndent, itemType); - html = html.replace(/<\/li>$/, "") + nestedHtml + ""; - i = consumed; - continue; - } - if (lineIndent < indent || itemType !== type) { - break; - } - html += `
  • ${match[3]}`; - // Check for next line being a nested list - if (i + 1 < lines.length) { - const nextMatch = lines[i + 1].match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); - if (nextMatch) { - const nextIndent = nextMatch[1].replace(/\t/g, " ").length; - const nextType = /\d+\./.test(nextMatch[2]) ? "ol" : "ul"; - if (nextIndent > lineIndent) { - const [nestedHtml, consumed] = parseList( - i + 1, - nextIndent, - nextType, - ); - html += nestedHtml; - i = consumed - 1; - } - } - } - html += "
  • "; - i++; - } - html += ``; - return [html, i]; + return processedText; + } catch (e) { + throw new Error(`[preProcessBasicMarkup] Error: ${e}`); } - if (!lines.length) return ""; - const firstLine = lines[0]; - const match = firstLine.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); - const indent = match ? match[1].replace(/\t/g, " ").length : 0; - const type = typeHint || (match && /\d+\./.test(match[2]) ? "ol" : "ul"); - const [html] = parseList(0, indent, type); - return html; } -function processBasicFormatting(content: string): string { - if (!content) return ""; - - let processedText = content; - +export async function postProcessBasicMarkup(text: string, ndk?: NDK): Promise { try { - // Sanitize Alexandria Nostr links before further processing - processedText = replaceAlexandriaNostrLinks(processedText); - - // Process markup images first - processedText = processedText.replace(MARKUP_IMAGE, (match, alt, url) => { - // Clean the URL and alt text - const cleanUrl = url.trim(); - const cleanAlt = alt ? alt.trim() : ""; - return processImageWithReveal(cleanUrl, cleanAlt); - }); - - // Process markup links - processedText = processedText.replace( - MARKUP_LINK, - (_match, text, url) => - `${text}`, - ); - - // Process WebSocket URLs using shared services - processedText = processWebSocketUrls(processedText); - - // Process direct media URLs and auto-link all URLs - processedText = processedText.replace(DIRECT_LINK, (match) => { - return processMediaUrl(match); - }); - - // Process text formatting using shared services - processedText = processBasicTextFormatting(processedText); + // Process Nostr identifiers last + let processedText = await processNostrIdentifiersInText(text, ndk); - // Process hashtags using shared services - processedText = processHashtags(processedText); + // Replace wikilinks + processedText = processWikilinks(processedText); - // --- Improved List Grouping and Parsing --- - const lines = processedText.split("\n"); - let output = ""; - let buffer: string[] = []; - let inList = false; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (/^([ \t]*)([*+-]|\d+\.)[ \t]+/.test(line)) { - buffer.push(line); - inList = true; - } else { - if (inList) { - const firstLine = buffer[0]; - const isOrdered = /^\s*\d+\.\s+/.test(firstLine); - output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); - buffer = []; - inList = false; - } - output += (output && !output.endsWith("\n") ? "\n" : "") + line + "\n"; - } - } - if (buffer.length) { - const firstLine = buffer[0]; - const isOrdered = /^\s*\d+\.\s+/.test(firstLine); - output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); - } - processedText = output; - // --- End Improved List Grouping and Parsing --- - } catch (e: unknown) { - console.error("Error in processBasicFormatting:", e); + return processedText; + } catch (e) { + throw new Error(`[postProcessBasicMarkup] Error: ${e}`); } - - return processedText; } export async function parseBasicMarkup(text: string, ndk?: NDK): Promise { if (!text) return ""; try { - // Process basic text formatting first - let processedText = processBasicFormatting(text); - - // Process emoji shortcuts - processedText = processEmojiShortcodes(processedText); - - // Process blockquotes - processedText = processBlockquotes(processedText); + let processedText = preProcessBasicMarkup(text); // Process paragraphs - split by double newlines and wrap in p tags // Skip wrapping if content already contains block-level elements @@ -251,10 +68,7 @@ export async function parseBasicMarkup(text: string, ndk?: NDK): Promise .join("\n"); // Process Nostr identifiers last - processedText = await processNostrIdentifiersInText(processedText, ndk); - - // Replace wikilinks - processedText = processWikilinks(processedText); + processedText = await postProcessBasicMarkup(processedText, ndk); return processedText; } catch (e) { diff --git a/src/lib/utils/markup/embeddedMarkupParser.ts b/src/lib/utils/markup/embeddedMarkupParser.ts index 8813c92..8a757c6 100644 --- a/src/lib/utils/markup/embeddedMarkupParser.ts +++ b/src/lib/utils/markup/embeddedMarkupParser.ts @@ -1,220 +1,5 @@ -import { nip19 } from "nostr-tools"; -import { - processBasicTextFormatting, - processBlockquotes, - processEmojiShortcodes, - processHashtags, - processImageWithReveal, - processMediaUrl, - processNostrIdentifiersInText, - processNostrIdentifiersWithEmbeddedEvents, - processWebSocketUrls, - processWikilinks, - stripTrackingParams, -} from "./markupServices.ts"; - -/* Regex constants for basic markup parsing */ - -// Links and media -const MARKUP_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; -const MARKUP_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; -// AI-NOTE: 2025-01-24 - Added negative lookbehind (?"]+)(?!["'])/g; - -// Add this helper function near the top: -function replaceAlexandriaNostrLinks(text: string): string { - // Regex for Alexandria/localhost URLs - const alexandriaPattern = - /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i; - // Regex for bech32 Nostr identifiers - const bech32Pattern = /(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/; - // Regex for 64-char hex - const hexPattern = /\b[a-fA-F0-9]{64}\b/; - - // 1. Alexandria/localhost markup links - text = text.replace( - /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, - (match, _label, url) => { - if (alexandriaPattern.test(url)) { - if (/[?&]d=/.test(url)) return match; - const hexMatch = url.match(hexPattern); - if (hexMatch) { - try { - const nevent = nip19.neventEncode({ id: hexMatch[0] }); - return `nostr:${nevent}`; - } catch { - return match; - } - } - const bech32Match = url.match(bech32Pattern); - if (bech32Match) { - return `nostr:${bech32Match[0]}`; - } - } - return match; - }, - ); - - // 2. Alexandria/localhost bare URLs and non-Alexandria/localhost URLs with Nostr identifiers - text = text.replace(/https?:\/\/[^\s)\]]+/g, (url) => { - if (alexandriaPattern.test(url)) { - if (/[?&]d=/.test(url)) return url; - const hexMatch = url.match(hexPattern); - if (hexMatch) { - try { - const nevent = nip19.neventEncode({ id: hexMatch[0] }); - return `nostr:${nevent}`; - } catch { - return url; - } - } - const bech32Match = url.match(bech32Pattern); - if (bech32Match) { - return `nostr:${bech32Match[0]}`; - } - } - // For non-Alexandria/localhost URLs, just return the URL as-is - return url; - }); - - return text; -} - -function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { - function parseList( - start: number, - indent: number, - type: "ol" | "ul", - ): [string, number] { - let html = ""; - let i = start; - html += `<${type} class="${ - type === "ol" ? "list-decimal" : "list-disc" - } ml-6 mb-2">`; - while (i < lines.length) { - const line = lines[i]; - const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/); - if (!match) break; - const lineIndent = match[1].replace(/\t/g, " ").length; - const isOrdered = /\d+\./.test(match[2]); - const itemType = isOrdered ? "ol" : "ul"; - if (lineIndent > indent) { - // Nested list - const [nestedHtml, consumed] = parseList(i, lineIndent, itemType); - html = html.replace(/<\/li>$/, "") + nestedHtml + ""; - i = consumed; - continue; - } - if (lineIndent < indent || itemType !== type) { - break; - } - html += `
  • ${match[3]}`; - // Check for next line being a nested list - if (i + 1 < lines.length) { - const nextMatch = lines[i + 1].match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); - if (nextMatch) { - const nextIndent = nextMatch[1].replace(/\t/g, " ").length; - const nextType = /\d+\./.test(nextMatch[2]) ? "ol" : "ul"; - if (nextIndent > lineIndent) { - const [nestedHtml, consumed] = parseList( - i + 1, - nextIndent, - nextType, - ); - html += nestedHtml; - i = consumed - 1; - } - } - } - html += "
  • "; - i++; - } - html += ``; - return [html, i]; - } - if (!lines.length) return ""; - const firstLine = lines[0]; - const match = firstLine.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); - const indent = match ? match[1].replace(/\t/g, " ").length : 0; - const type = typeHint || (match && /\d+\./.test(match[2]) ? "ol" : "ul"); - const [html] = parseList(0, indent, type); - return html; -} - -function processBasicFormatting(content: string): string { - if (!content) return ""; - - let processedText = content; - - try { - // Sanitize Alexandria Nostr links before further processing - processedText = replaceAlexandriaNostrLinks(processedText); - - // Process markup images first - processedText = processedText.replace(MARKUP_IMAGE, (match, alt, url) => { - // Clean the URL and alt text - const cleanUrl = url.trim(); - const cleanAlt = alt ? alt.trim() : ""; - return processImageWithReveal(cleanUrl, cleanAlt); - }); - - // Process markup links - processedText = processedText.replace( - MARKUP_LINK, - (_match, text, url) => - `${text}`, - ); - - // Process WebSocket URLs using shared services - processedText = processWebSocketUrls(processedText); - - // Process direct media URLs and auto-link all URLs - processedText = processedText.replace(DIRECT_LINK, (match) => { - return processMediaUrl(match); - }); - - // Process text formatting using shared services - processedText = processBasicTextFormatting(processedText); - - // Process hashtags using shared services - processedText = processHashtags(processedText); - - // --- Improved List Grouping and Parsing --- - const lines = processedText.split("\n"); - let output = ""; - let buffer: string[] = []; - let inList = false; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (/^([ \t]*)([*+-]|\d+\.)[ \t]+/.test(line)) { - buffer.push(line); - inList = true; - } else { - if (inList) { - const firstLine = buffer[0]; - const isOrdered = /^\s*\d+\.\s+/.test(firstLine); - output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); - buffer = []; - inList = false; - } - output += (output && !output.endsWith("\n") ? "\n" : "") + line + "\n"; - } - } - if (buffer.length) { - const firstLine = buffer[0]; - const isOrdered = /^\s*\d+\.\s+/.test(firstLine); - output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); - } - processedText = output; - // --- End Improved List Grouping and Parsing --- - } catch (e: unknown) { - console.error("Error in processBasicFormatting:", e); - } - - return processedText; -} +import { postProcessBasicMarkup, preProcessBasicMarkup } from "./basicMarkupParser.ts"; +import { processNostrIdentifiersWithEmbeddedEvents } from "./markupUtils.ts"; /** * Parse markup with support for embedded Nostr events @@ -228,14 +13,7 @@ export async function parseEmbeddedMarkup( if (!text) return ""; try { - // Process basic text formatting first - let processedText = processBasicFormatting(text); - - // Process emoji shortcuts - processedText = processEmojiShortcodes(processedText); - - // Process blockquotes - processedText = processBlockquotes(processedText); + let processedText = preProcessBasicMarkup(text); // Process paragraphs - split by double newlines and wrap in p tags // Skip wrapping if content already contains block-level elements @@ -255,17 +33,14 @@ export async function parseEmbeddedMarkup( }) .join("\n"); - // Process profile identifiers (npub, nprofile) first using the regular processor - processedText = await processNostrIdentifiersInText(processedText); - - // Then process event identifiers with embedded events (only event-related identifiers) + // Process event identifiers with embedded events (only event-related identifiers) processedText = processNostrIdentifiersWithEmbeddedEvents( processedText, nestingLevel, ); - // Replace wikilinks - processedText = processWikilinks(processedText); + // Process profile identifiers (npub, nprofile) using the regular processor + processedText = await postProcessBasicMarkup(processedText); return processedText; } catch (e: unknown) { diff --git a/src/lib/utils/markup/markupServices.ts b/src/lib/utils/markup/markupUtils.ts similarity index 70% rename from src/lib/utils/markup/markupServices.ts rename to src/lib/utils/markup/markupUtils.ts index 52532f8..9417386 100644 --- a/src/lib/utils/markup/markupServices.ts +++ b/src/lib/utils/markup/markupUtils.ts @@ -2,6 +2,7 @@ import NDK from "@nostr-dev-kit/ndk"; import { createProfileLink, getUserMetadata, + nip19, NOSTR_PROFILE_REGEX, } from "../nostrUtils.ts"; @@ -14,6 +15,207 @@ const AUDIO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp3|wav|ogg|m4a)(?:[^\s<]*)?/i; const YOUTUBE_URL_REGEX = /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/; +// Links and media +const MARKUP_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; +const MARKUP_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; +// AI-NOTE: Added negative lookbehind (?"]+)(?!["'])/g; + +// Add this helper function near the top: +export function replaceAlexandriaNostrLinks(text: string): string { + // Regex for Alexandria/localhost URLs + const alexandriaPattern = + /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i; + // Regex for bech32 Nostr identifiers + const bech32Pattern = /(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/; + // Regex for 64-char hex + const hexPattern = /\b[a-fA-F0-9]{64}\b/; + + // 1. Alexandria/localhost markup links + text = text.replace( + /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, + (match, _label, url) => { + if (alexandriaPattern.test(url)) { + if (/[?&]d=/.test(url)) return match; + const hexMatch = url.match(hexPattern); + if (hexMatch) { + try { + const nevent = nip19.neventEncode({ id: hexMatch[0] }); + return `nostr:${nevent}`; + } catch { + return match; + } + } + const bech32Match = url.match(bech32Pattern); + if (bech32Match) { + return `nostr:${bech32Match[0]}`; + } + } + return match; + }, + ); + + // 2. Alexandria/localhost bare URLs and non-Alexandria/localhost URLs with Nostr identifiers + text = text.replace(/https?:\/\/[^\s)\]]+/g, (url) => { + if (alexandriaPattern.test(url)) { + if (/[?&]d=/.test(url)) return url; + const hexMatch = url.match(hexPattern); + if (hexMatch) { + try { + const nevent = nip19.neventEncode({ id: hexMatch[0] }); + return `nostr:${nevent}`; + } catch { + return url; + } + } + const bech32Match = url.match(bech32Pattern); + if (bech32Match) { + return `nostr:${bech32Match[0]}`; + } + } + // For non-Alexandria/localhost URLs, just return the URL as-is + return url; + }); + + return text; +} + +export function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { + function parseList( + start: number, + indent: number, + type: "ol" | "ul", + ): [string, number] { + let html = ""; + let i = start; + html += `<${type} class="${ + type === "ol" ? "list-decimal" : "list-disc" + } ml-6 mb-2">`; + while (i < lines.length) { + const line = lines[i]; + const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/); + if (!match) break; + const lineIndent = match[1].replace(/\t/g, " ").length; + const isOrdered = /\d+\./.test(match[2]); + const itemType = isOrdered ? "ol" : "ul"; + if (lineIndent > indent) { + // Nested list + const [nestedHtml, consumed] = parseList(i, lineIndent, itemType); + html = html.replace(/<\/li>$/, "") + nestedHtml + ""; + i = consumed; + continue; + } + if (lineIndent < indent || itemType !== type) { + break; + } + html += `
  • ${match[3]}`; + // Check for next line being a nested list + if (i + 1 < lines.length) { + const nextMatch = lines[i + 1].match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); + if (nextMatch) { + const nextIndent = nextMatch[1].replace(/\t/g, " ").length; + const nextType = /\d+\./.test(nextMatch[2]) ? "ol" : "ul"; + if (nextIndent > lineIndent) { + const [nestedHtml, consumed] = parseList( + i + 1, + nextIndent, + nextType, + ); + html += nestedHtml; + i = consumed - 1; + } + } + } + html += "
  • "; + i++; + } + html += ``; + return [html, i]; + } + if (!lines.length) return ""; + const firstLine = lines[0]; + const match = firstLine.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); + const indent = match ? match[1].replace(/\t/g, " ").length : 0; + const type = typeHint || (match && /\d+\./.test(match[2]) ? "ol" : "ul"); + const [html] = parseList(0, indent, type); + return html; +} + +export function processBasicFormatting(content: string): string { + if (!content) return ""; + + let processedText = content; + + try { + // Sanitize Alexandria Nostr links before further processing + processedText = replaceAlexandriaNostrLinks(processedText); + + // Process markup images first + processedText = processedText.replace(MARKUP_IMAGE, (match, alt, url) => { + // Clean the URL and alt text + const cleanUrl = url.trim(); + const cleanAlt = alt ? alt.trim() : ""; + return processImageWithReveal(cleanUrl, cleanAlt); + }); + + // Process markup links + processedText = processedText.replace( + MARKUP_LINK, + (_match, text, url) => + `${text}`, + ); + + // Process WebSocket URLs using shared services + processedText = processWebSocketUrls(processedText); + + // Process direct media URLs and auto-link all URLs + processedText = processedText.replace(DIRECT_LINK, (match) => { + return processMediaUrl(match); + }); + + // Process text formatting using shared services + processedText = processBasicTextFormatting(processedText); + + // Process hashtags using shared services + processedText = processHashtags(processedText); + + // --- Improved List Grouping and Parsing --- + const lines = processedText.split("\n"); + let output = ""; + let buffer: string[] = []; + let inList = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (/^([ \t]*)([*+-]|\d+\.)[ \t]+/.test(line)) { + buffer.push(line); + inList = true; + } else { + if (inList) { + const firstLine = buffer[0]; + const isOrdered = /^\s*\d+\.\s+/.test(firstLine); + output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); + buffer = []; + inList = false; + } + output += (output && !output.endsWith("\n") ? "\n" : "") + line + "\n"; + } + } + if (buffer.length) { + const firstLine = buffer[0]; + const isOrdered = /^\s*\d+\.\s+/.test(firstLine); + output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); + } + processedText = output; + // --- End Improved List Grouping and Parsing --- + } catch (e: unknown) { + console.error("Error in processBasicFormatting:", e); + } + + return processedText; +} + /** * Shared service for processing images with expand functionality */