From 13a2932cde72e1ae36b450fd58d40808152ec8c4 Mon Sep 17 00:00:00 2001
From: Silberengel
Date: Sat, 19 Apr 2025 00:19:53 +0200
Subject: [PATCH] Fixed most of the regex on the page.
---
src/lib/components/LoginModal.svelte | 36 +++
src/lib/components/Navigation.svelte | 1 +
src/lib/utils/markdownParser.ts | 340 +++++++++++++++++++++++
src/routes/contact/+page.svelte | 390 +++++++++++++++++++++++++++
4 files changed, 767 insertions(+)
create mode 100644 src/lib/components/LoginModal.svelte
create mode 100644 src/lib/utils/markdownParser.ts
create mode 100644 src/routes/contact/+page.svelte
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte
new file mode 100644
index 0000000..b1c5a30
--- /dev/null
+++ b/src/lib/components/LoginModal.svelte
@@ -0,0 +1,36 @@
+
+
+{#if show}
+
+
+
+
+
+
Login Required
+
+ ×
+
+
+
+
+
+
+ You need to be logged in to submit an issue. Your form data will be preserved.
+
+
+
+
+
+
+
+
+{/if}
\ No newline at end of file
diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte
index 2ac6133..e6ca543 100644
--- a/src/lib/components/Navigation.svelte
+++ b/src/lib/components/Navigation.svelte
@@ -21,6 +21,7 @@
Publish
Visualize
About
+ Contact
diff --git a/src/lib/utils/markdownParser.ts b/src/lib/utils/markdownParser.ts
new file mode 100644
index 0000000..e0e09ed
--- /dev/null
+++ b/src/lib/utils/markdownParser.ts
@@ -0,0 +1,340 @@
+/**
+ * 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(` ${item} `);
+ });
+ processed.push(`${listType}>`);
+ inList = false;
+ currentList = [];
+ }
+ processed.push(line);
+ }
+
+ // Reset regex lastIndex
+ ORDERED_LIST_REGEX.lastIndex = 0;
+ UNORDERED_LIST_REGEX.lastIndex = 0;
+ }
+
+ 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(` ${item} `);
+ });
+ processed.push(`${listType}>`);
+ }
+
+ return processed.join('\n');
+}
+
+/**
+ * Process blockquotes using placeholder approach
+ */
+function processBlockquotes(text: string): string {
+ const blockquotes: Array<{id: string, content: string}> = [];
+ let processedText = text;
+
+ // Extract and save blockquotes
+ processedText = processedText.replace(BLOCKQUOTE_REGEX, (match) => {
+ const id = `BLOCKQUOTE_${blockquotes.length}`;
+ const cleanContent = match
+ .split('\n')
+ .map(line => line.replace(/^>[ \t]*/, ''))
+ .join('\n')
+ .trim();
+
+ blockquotes.push({
+ id,
+ content: `${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 } {
+ const footnotes = new Map();
+ let counter = 0;
+
+ // Extract footnote definitions
+ text = text.replace(FOOTNOTE_DEFINITION_REGEX, (match, id, content) => {
+ const cleanId = id.replace('^', '');
+ footnotes.set(cleanId, content.trim());
+ return '';
+ });
+
+ // Replace references
+ text = text.replace(FOOTNOTE_REFERENCE_REGEX, (match, id) => {
+ const cleanId = id.replace('^', '');
+ if (footnotes.has(cleanId)) {
+ counter++;
+ return `[${counter}] `;
+ }
+ return match;
+ });
+
+ // Add footnotes section if we have any
+ if (footnotes.size > 0) {
+ text += '\n';
+ }
+
+ return { text, footnotes };
+}
+
+/**
+ * Parse markdown text to HTML with special handling for nostr identifiers
+ */
+export async function parseMarkdown(text: string): Promise {
+ if (!text) return '';
+
+ // First, process code blocks (protect these from HTML escaping)
+ let html = processCode(text); // still escape HTML *inside* code blocks
+
+ // 👉 NEW: process blockquotes *before* the rest of HTML is escaped
+ html = processBlockquotes(html);
+
+ // Process nostr identifiers
+ const npubMatches = Array.from(html.matchAll(NOSTR_NPUB_REGEX));
+ const npubPromises = npubMatches.map(async match => {
+ const [fullMatch, npub] = match;
+ const metadata = await getUserMetadata(npub);
+ const displayText = metadata.displayName || metadata.name || `${npub.slice(0, 8)}...${npub.slice(-4)}`;
+ return { fullMatch, npub, displayText };
+ });
+
+ const npubResults = await Promise.all(npubPromises);
+ for (const { fullMatch, npub, displayText } of npubResults) {
+ html = html.replace(
+ fullMatch,
+ `@${displayText} `
+ );
+ }
+
+ // Process lists
+ html = processLists(html);
+
+ // Process footnotes
+ const { text: processedHtml } = processFootnotes(html);
+ html = processedHtml;
+
+ // Process basic markdown elements
+ html = html.replace(BOLD_REGEX, '$1$2 ');
+ html = html.replace(ITALIC_REGEX, '$1 ');
+ html = html.replace(HEADING_REGEX, (match, hashes, content) => {
+ const level = hashes.length;
+ const sizes = ['text-2xl', 'text-xl', 'text-lg', 'text-base', 'text-sm', 'text-xs'];
+ return `${content.trim()} `;
+ });
+
+ // Process links and images
+ html = html.replace(IMAGE_REGEX, ' ');
+ html = html.replace(LINK_REGEX, '$1 ');
+
+ // Process hashtags
+ html = html.replace(HASHTAG_REGEX, '#$1 ');
+
+ // Process horizontal rules
+ html = html.replace(HORIZONTAL_RULE_REGEX, ' ');
+
+ // Handle paragraphs and line breaks
+ html = html.replace(/\n{2,}/g, '
');
+ 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, '\\$&');
+}
diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte
new file mode 100644
index 0000000..3cfee2d
--- /dev/null
+++ b/src/routes/contact/+page.svelte
@@ -0,0 +1,390 @@
+
+
+
+
+ Contact GitCitadel
+
+
+ Make sure that you follow us on GitHub and Geyserfund .
+
+
+
+ You can contact us on Nostr npub1s3h…75wz or you can view submitted issues on the Alexandria repo page.
+
+
+ Submit an issue
+
+
+ If you are logged into the Alexandria web application (using the button at the top-right of the window), then you can use the form, below, to submit an issue, that will appear on our repo page.
+
+
+
+
+
+
+
+
+ showLoginModal = false}
+/>
+
+