diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 17b8b87..22e9719 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,7 +1,7 @@ export const wikiKind = 30818; export const indexKind = 30040; export const zettelKinds = [ 30041, 30818 ]; -export const standardRelays = [ 'wss://thecitadel.nostr1.com', 'wss://relay.noswhere.com' ]; +export const standardRelays = [ 'wss://thecitadel.nostr1.com', 'wss://theforest.nostr1.com' ]; export const bootstrapRelays = [ 'wss://purplepag.es', 'wss://relay.noswhere.com' ]; export enum FeedType { diff --git a/src/lib/utils/advancedMarkdownParser.ts b/src/lib/utils/advancedMarkdownParser.ts index 6ddfe56..07851c7 100644 --- a/src/lib/utils/advancedMarkdownParser.ts +++ b/src/lib/utils/advancedMarkdownParser.ts @@ -2,7 +2,6 @@ import { parseBasicMarkdown } from './basicMarkdownParser'; import hljs from 'highlight.js'; import 'highlight.js/lib/common'; // Import common languages import 'highlight.js/styles/github-dark.css'; // Dark theme only -import { processNostrIdentifiers } from './nostrUtils'; // Register common languages hljs.configure({ @@ -13,23 +12,10 @@ hljs.configure({ const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm; const ALTERNATE_HEADING_REGEX = /^([^\n]+)\n(=+|-+)\n/gm; const INLINE_CODE_REGEX = /`([^`\n]+)`/g; -const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g; -const IMAGE_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g; const HORIZONTAL_RULE_REGEX = /^(?:[-*_]\s*){3,}$/gm; const FOOTNOTE_REFERENCE_REGEX = /\[\^([^\]]+)\]/g; const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm; -interface Footnote { - id: string; - text: string; - referenceCount: number; -} - -interface FootnoteReference { - id: string; - count: number; -} - /** * Process headings (both styles) */ @@ -123,23 +109,6 @@ function processTables(content: string): string { } } -/** - * Process links and images - */ -function processLinksAndImages(content: string): string { - // Process images first to avoid conflicts with links - let processedContent = content.replace(IMAGE_REGEX, - '$1' - ); - - // Process links - processedContent = processedContent.replace(LINK_REGEX, - '$1' - ); - - return processedContent; -} - /** * Process horizontal rules */ @@ -367,21 +336,18 @@ function restoreCodeBlocks(text: string, blocks: Map): string { * Parse markdown text with advanced formatting */ export async function parseAdvancedMarkdown(text: string): Promise { + if (!text) return ''; + try { - if (!text) return ''; - // Step 1: Extract and save code blocks first const { text: withoutCode, blocks } = processCodeBlocks(text); - - // Step 2: Process all other markdown let processedText = withoutCode; - // Process block-level elements + // Step 2: Process block-level elements processedText = processTables(processedText); processedText = processBlockquotes(processedText); processedText = processHeadings(processedText); processedText = processHorizontalRules(processedText); - processedText = processLinksAndImages(processedText); // Process inline elements processedText = processedText.replace(INLINE_CODE_REGEX, (_, code) => { @@ -395,11 +361,10 @@ export async function parseAdvancedMarkdown(text: string): Promise { return `${escapedCode}`; }); - // Process footnotes before basic markdown to prevent unwanted paragraph tags + // Process footnotes processedText = processFootnotes(processedText); - // Process async elements - processedText = await processNostrIdentifiers(processedText); + // Process basic markdown (which will also handle Nostr identifiers) processedText = await parseBasicMarkdown(processedText); // Step 3: Restore code blocks @@ -408,9 +373,6 @@ export async function parseAdvancedMarkdown(text: string): Promise { return processedText; } catch (error) { console.error('Error in parseAdvancedMarkdown:', error); - if (error instanceof Error) { - return `
Error processing markdown: ${error.message}
`; - } - return '
An error occurred while processing the markdown
'; + return `
Error processing markdown: ${error instanceof Error ? error.message : 'Unknown error'}
`; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/lib/utils/basicMarkdownParser.ts b/src/lib/utils/basicMarkdownParser.ts index 571f756..c30e442 100644 --- a/src/lib/utils/basicMarkdownParser.ts +++ b/src/lib/utils/basicMarkdownParser.ts @@ -7,98 +7,122 @@ const STRIKETHROUGH_REGEX = /~~([^~\n]+)~~|~([^~\n]+)~/g; const HASHTAG_REGEX = /(?[ \t]?.*)(?:\n\1[ \t]*(?!>).*)*$/gm; -interface ListItem { - type: 'ul' | 'ol'; - indent: number; - content: string; - marker: string; -} +// List regex patterns +const UNORDERED_LIST_REGEX = /^(\s*[-*+]\s+)(.*?)$/gm; +const ORDERED_LIST_REGEX = /^(\s*\d+\.\s+)(.*?)$/gm; + +// Markdown patterns +const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; +const MARKDOWN_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; + +// URL patterns +const WSS_URL = /wss:\/\/[^\s<>"]+/g; +const DIRECT_LINK = /(?"]+)(?!["'])/g; + +// Media URL patterns +const IMAGE_URL_REGEX = /https?:\/\/[^\s<]+\.(?:jpg|jpeg|gif|png|webp)(?:[^\s<]*)?/i; +const VIDEO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp4|webm|mov|avi)(?:[^\s<]*)?/i; +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<]*)?/i; -// HTML escape function -function escapeHtml(text: string): string { - const htmlEscapes: { [key: string]: string } = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - return text.replace(/[&<>"']/g, char => htmlEscapes[char]); -} -/** - * Process paragraphs and line breaks - */ -function processParagraphs(content: string): string { +function processBasicFormatting(content: string): string { + if (!content) return ''; + + let processedText = content; + try { - if (!content) return ''; - - // Split content into paragraphs (double line breaks) - const paragraphs = content.split(/\n\s*\n/); - - // Process each paragraph - return paragraphs.map(para => { - if (!para.trim()) return ''; + // Process Markdown images first + processedText = processedText.replace(MARKDOWN_IMAGE, (match, alt, url) => { + if (YOUTUBE_URL_REGEX.test(url)) { + const videoId = extractYouTubeVideoId(url); + if (videoId) { + return ``; + } + } - // Handle single line breaks within paragraphs - const lines = para.split('\n'); + if (VIDEO_URL_REGEX.test(url)) { + return ``; + } - // Join lines with normal line breaks and add br after paragraph - return `

${lines.join('\n')}


`; - }).filter(Boolean).join('\n'); - } catch (error) { - console.error('Error in processParagraphs:', error); - return content; - } -} + if (AUDIO_URL_REGEX.test(url)) { + return ``; + } + + return `${alt}`; + }); -/** - * Process basic text formatting (bold, italic, strikethrough, hashtags, inline code) - */ -function processBasicFormatting(content: string): string { - try { - if (!content) return ''; - - // Process bold first to avoid conflicts - content = content.replace(BOLD_REGEX, '$2'); + // Process Markdown links + processedText = processedText.replace(MARKDOWN_LINK, (match, text, url) => + `${text}` + ); + + // Process WebSocket URLs + processedText = processedText.replace(WSS_URL, match => { + // Remove 'wss://' from the start and any trailing slashes + const cleanUrl = match.slice(6).replace(/\/+$/, ''); + return `${match}`; + }); + + // Process direct media URLs + processedText = processedText.replace(DIRECT_LINK, match => { + if (YOUTUBE_URL_REGEX.test(match)) { + const videoId = extractYouTubeVideoId(match); + if (videoId) { + return ``; + } + } + + if (VIDEO_URL_REGEX.test(match)) { + return ``; + } + + if (AUDIO_URL_REGEX.test(match)) { + return ``; + } + + if (IMAGE_URL_REGEX.test(match)) { + return `Embedded media`; + } + + return `${match}`; + }); - // Then process italic, handling both single and double underscores - content = content.replace(ITALIC_REGEX, match => { + // Process text formatting + processedText = processedText.replace(BOLD_REGEX, '$2'); + processedText = processedText.replace(ITALIC_REGEX, match => { const text = match.replace(/^_+|_+$/g, ''); return `${text}`; }); - - // Then process strikethrough, handling both single and double tildes - content = content.replace(STRIKETHROUGH_REGEX, (match, doubleText, singleText) => { + processedText = processedText.replace(STRIKETHROUGH_REGEX, (match, doubleText, singleText) => { const text = doubleText || singleText; return `${text}`; }); - - // Finally process hashtags - style them with a lighter color - content = content.replace(HASHTAG_REGEX, '#$1'); - - return content; + + // Process hashtags + processedText = processedText.replace(HASHTAG_REGEX, '#$1'); } catch (error) { console.error('Error in processBasicFormatting:', error); - return content; } + + return processedText; +} + +// Helper function to extract YouTube video ID +function extractYouTubeVideoId(url: string): string | null { + const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/); + return match ? match[1] : null; } -/** - * Process blockquotes - */ function processBlockquotes(content: string): string { try { if (!content) return ''; return content.replace(BLOCKQUOTE_REGEX, match => { - // Split into lines and process each line const lines = match.split('\n').map(line => { - // Remove the '>' marker and trim any whitespace after it return line.replace(/^[ \t]*>[ \t]?/, '').trim(); }); - // Join the lines with proper spacing and wrap in blockquote return `
${ lines.join('\n') }
`; @@ -109,113 +133,43 @@ function processBlockquotes(content: string): string { } } -/** - * Calculate indentation level from spaces - */ -function getIndentLevel(spaces: string): number { - return Math.floor(spaces.length / 2); -} - -/** - * Process lists (ordered and unordered) - */ -function processLists(content: string): string { - const lines = content.split('\n'); - const processed: string[] = []; - const listStack: { type: 'ol' | 'ul', items: string[], level: number }[] = []; - - function closeList() { - if (listStack.length > 0) { - const list = listStack.pop()!; - const listType = list.type; - const listClass = listType === 'ol' ? 'list-decimal' : 'list-disc'; - const indentClass = list.level > 0 ? 'ml-6' : 'ml-4'; - let listHtml = `<${listType} class="${listClass} ${indentClass} my-2 space-y-2">`; - list.items.forEach(item => { - listHtml += `\n
  • ${item}
  • `; - }); - listHtml += `\n`; - - if (listStack.length > 0) { - // If we're in a nested list, add this as an item to the parent - const parentList = listStack[listStack.length - 1]; - const lastItem = parentList.items.pop()!; - parentList.items.push(lastItem + '\n' + listHtml); - } else { - processed.push(listHtml); - } - } - } - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - // Count leading spaces to determine nesting level - const leadingSpaces = line.match(/^(\s*)/)?.[0]?.length ?? 0; - const effectiveLevel = Math.floor(leadingSpaces / 2); // 2 spaces per level - - // Trim the line and check for list markers - const trimmedLine = line.trim(); - const orderedMatch = trimmedLine.match(/^(\d+)\.[ \t]+(.+)$/); - const unorderedMatch = trimmedLine.match(/^[-*][ \t]+(.+)$/); - - if (orderedMatch || unorderedMatch) { - const content = orderedMatch ? orderedMatch[2] : (unorderedMatch && unorderedMatch[1]) || ''; - const type = orderedMatch ? 'ol' : 'ul'; - - // Close any lists that are at a deeper level - while (listStack.length > 0 && listStack[listStack.length - 1].level > effectiveLevel) { - closeList(); - } - - // If we're at a new level, start a new list - if (listStack.length === 0 || listStack[listStack.length - 1].level < effectiveLevel) { - listStack.push({ type, items: [], level: effectiveLevel }); - } - // If we're at the same level but different type, close the current list and start a new one - else if (listStack[listStack.length - 1].type !== type && listStack[listStack.length - 1].level === effectiveLevel) { - closeList(); - listStack.push({ type, items: [], level: effectiveLevel }); - } - - // Add the item to the current list - listStack[listStack.length - 1].items.push(content); - } else { - // Not a list item - close all open lists and add the line - while (listStack.length > 0) { - closeList(); - } - processed.push(line); - } - } - - // Close any remaining open lists - while (listStack.length > 0) { - closeList(); - } - - return processed.join('\n'); -} - -/** - * Parse markdown text with basic formatting - */ export async function parseBasicMarkdown(text: string): Promise { + if (!text) return ''; + try { - if (!text) return ''; - - let processedText = text; - - // Process lists first to handle indentation properly - processedText = processLists(processedText); - - // Process blockquotes next + // Process basic text formatting first + let processedText = processBasicFormatting(text); + + // Process lists - handle ordered lists first + processedText = processedText + // Process ordered lists + .replace(ORDERED_LIST_REGEX, (match, marker, content) => { + // Count leading spaces to determine nesting level + const indent = marker.match(/^\s*/)[0].length; + const extraIndent = indent > 0 ? ` ml-${indent * 4}` : ''; + return `
  • ${content}
  • `; + }) + .replace(/.*?<\/li>\n?/gs, '
      $&
    ') + + // Process unordered lists + .replace(UNORDERED_LIST_REGEX, (match, marker, content) => { + // Count leading spaces to determine nesting level + const indent = marker.match(/^\s*/)[0].length; + const extraIndent = indent > 0 ? ` ml-${indent * 4}` : ''; + return `
  • ${content}
  • `; + }) + .replace(/.*?<\/li>\n?/gs, '
      $&
    '); + + // Process blockquotes processedText = processBlockquotes(processedText); - - // Process paragraphs - processedText = processParagraphs(processedText); - - // Process basic text formatting - processedText = processBasicFormatting(processedText); + + // Process paragraphs - split by double newlines and wrap in p tags + processedText = processedText + .split(/\n\n+/) + .map(para => para.trim()) + .filter(para => para.length > 0) + .map(para => `

    ${para}

    `) + .join('\n'); // Process Nostr identifiers last processedText = await processNostrIdentifiers(processedText); @@ -223,9 +177,6 @@ export async function parseBasicMarkdown(text: string): Promise { return processedText; } catch (error) { console.error('Error in parseBasicMarkdown:', error); - if (error instanceof Error) { - return `
    Error processing markdown: ${error.message}
    `; - } - return '
    An error occurred while processing the markdown
    '; + return `
    Error processing markdown: ${error instanceof Error ? error.message : 'Unknown error'}
    `; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 60db82f..39c3ec4 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -118,6 +118,7 @@ function createNoteLink(identifier: string): string { * Process Nostr identifiers in text */ export async function processNostrIdentifiers(content: string): Promise { + console.log('Processing Nostr identifiers:', { input: content }); let processedContent = content; // Process profiles (npub and nprofile)