Browse Source

Reorganize markup processing code to reduce duplication

master
buttercat1791 7 months ago
parent
commit
134ce9a52d
  1. 49
      src/lib/utils/markup/asciidoctorPostProcessor.ts
  2. 232
      src/lib/utils/markup/basicMarkupParser.ts
  3. 237
      src/lib/utils/markup/embeddedMarkupParser.ts
  4. 202
      src/lib/utils/markup/markupUtils.ts

49
src/lib/utils/markup/asciidoctorPostProcessor.ts

@ -3,54 +3,7 @@ import { @@ -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<string> {
// Helper to check if a match is within an existing <a> tag
function isWithinLink(text: string, index: number): boolean {
// Look backwards from the match position to find the nearest <a> tag
const before = text.slice(0, index);
const lastOpenTag = before.lastIndexOf("<a");
const lastCloseTag = before.lastIndexOf("</a>");
// If we find an opening <a> tag after the last closing </a> 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

232
src/lib/utils/markup/basicMarkupParser.ts

@ -1,233 +1,50 @@ @@ -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 (?<!\]\() to prevent processing URLs in markdown syntax
const DIRECT_LINK = /(?<!["'=])(?<!\]\()(https?:\/\/[^\s<>"]+)(?!["'])/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 + "</li>";
i = consumed;
continue;
}
if (lineIndent < indent || itemType !== type) {
break;
}
html += `<li class="mb-1">${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 += "</li>";
i++;
}
html += `</${type}>`;
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<string> {
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) =>
`<a href="${
stripTrackingParams(url)
}" class="text-primary-600 dark:text-primary-500 hover:underline" target="_blank" rel="noopener noreferrer">${text}</a>`,
);
// 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<string> {
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<string> @@ -251,10 +68,7 @@ export async function parseBasicMarkup(text: string, ndk?: NDK): Promise<string>
.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) {

237
src/lib/utils/markup/embeddedMarkupParser.ts

@ -1,220 +1,5 @@ @@ -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 (?<!\]\() to prevent processing URLs in markdown syntax
const DIRECT_LINK = /(?<!["'=])(?<!\]\()(https?:\/\/[^\s<>"]+)(?!["'])/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 + "</li>";
i = consumed;
continue;
}
if (lineIndent < indent || itemType !== type) {
break;
}
html += `<li class="mb-1">${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 += "</li>";
i++;
}
html += `</${type}>`;
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) =>
`<a href="${
stripTrackingParams(url)
}" class="text-primary-600 dark:text-primary-500 hover:underline" target="_blank" rel="noopener noreferrer">${text}</a>`,
);
// 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( @@ -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( @@ -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) {

202
src/lib/utils/markup/markupServices.ts → src/lib/utils/markup/markupUtils.ts

@ -2,6 +2,7 @@ import NDK from "@nostr-dev-kit/ndk"; @@ -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; @@ -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 (?<!\]\() to prevent processing URLs in markdown syntax
const DIRECT_LINK = /(?<!["'=])(?<!\]\()(https?:\/\/[^\s<>"]+)(?!["'])/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 + "</li>";
i = consumed;
continue;
}
if (lineIndent < indent || itemType !== type) {
break;
}
html += `<li class="mb-1">${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 += "</li>";
i++;
}
html += `</${type}>`;
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) =>
`<a href="${
stripTrackingParams(url)
}" class="text-primary-600 dark:text-primary-500 hover:underline" target="_blank" rel="noopener noreferrer">${text}</a>`,
);
// 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
*/
Loading…
Cancel
Save