From 2df526f33802b151966aa6c2dd47878a7659889b Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 12 May 2025 19:24:44 +0200 Subject: [PATCH] Make nostr address parsing conform to Elsat's replacement rules --- .vscode/settings.json | 5 +- src/app.css | 5 ++ .../utils/markdown/advancedMarkdownParser.ts | 22 ++++-- src/lib/utils/markdown/basicMarkdownParser.ts | 69 ++++++++++++++++++- src/lib/utils/markdown/markdownTestfile.md | 19 ++++- src/routes/contact/+page.svelte | 4 +- 6 files changed, 113 insertions(+), 11 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ce072c8..65737f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "editor.tabSize": 2 + "editor.tabSize": 2, +"files.associations": { + "*.css": "postcss" } +} \ No newline at end of file diff --git a/src/app.css b/src/app.css index cbfbb43..735d2df 100644 --- a/src/app.css +++ b/src/app.css @@ -251,6 +251,11 @@ @apply dark:text-white; } + /* Footnotes */ + :global(.footnotes-ol) { + list-style-type: decimal !important; + } + /* Rendered publication content */ .publication-leather { @apply flex flex-col space-y-4; diff --git a/src/lib/utils/markdown/advancedMarkdownParser.ts b/src/lib/utils/markdown/advancedMarkdownParser.ts index 9a29073..656fa73 100644 --- a/src/lib/utils/markdown/advancedMarkdownParser.ts +++ b/src/lib/utils/markdown/advancedMarkdownParser.ts @@ -20,16 +20,28 @@ const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm; * Process headings (both styles) */ function processHeadings(content: string): string { + // Tailwind classes for each heading level + const headingClasses = [ + 'text-4xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h1 + 'text-3xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h2 + 'text-2xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h3 + 'text-xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h4 + 'text-lg font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h5 + 'text-base font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h6 + ]; + // Process ATX-style headings (# Heading) let processedContent = content.replace(HEADING_REGEX, (_, level, text) => { - const headingLevel = level.length; - return `${text.trim()}`; + const headingLevel = Math.min(level.length, 6); + const classes = headingClasses[headingLevel - 1]; + return `${text.trim()}`; }); // Process Setext-style headings (Heading\n====) processedContent = processedContent.replace(ALTERNATE_HEADING_REGEX, (_, text, level) => { const headingLevel = level[0] === '=' ? 1 : 2; - return `${text.trim()}`; + const classes = headingClasses[headingLevel - 1]; + return `${text.trim()}`; }); return processedContent; @@ -148,9 +160,9 @@ function processFootnotes(content: string): string { return `[${refNum}]`; }); - // Add footnotes section if we have any + // Only render footnotes section if there are actual definitions and at least one reference if (footnotes.size > 0 && referenceOrder.length > 0) { - processedContent += '\n\n

Footnotes

\n
    \n'; + processedContent += '\n\n

    Footnotes

    \n
      \n'; // Only include each unique footnote once, in order of first reference const seen = new Set(); for (const { id, label } of referenceOrder) { diff --git a/src/lib/utils/markdown/basicMarkdownParser.ts b/src/lib/utils/markdown/basicMarkdownParser.ts index 68548ca..377a83d 100644 --- a/src/lib/utils/markdown/basicMarkdownParser.ts +++ b/src/lib/utils/markdown/basicMarkdownParser.ts @@ -1,5 +1,6 @@ import { processNostrIdentifiers } from '../nostrUtils'; import * as emoji from 'node-emoji'; +import { nip19 } from 'nostr-tools'; // Regular expressions for basic markdown elements const BOLD_REGEX = /(\*\*|[*])((?:[^*\n]|\*(?!\*))+)\1/g; @@ -26,6 +27,67 @@ 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; +// 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. Replace Markdown links ONLY if they match the criteria + text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (match, _label, url) => { + if (alexandriaPattern.test(url)) { + // Ignore d-tag URLs + if (/[?&]d=/.test(url)) return match; + // Convert hexid in URL to nevent if present + const hexMatch = url.match(hexPattern); + if (hexMatch) { + try { + const nevent = nip19.neventEncode({ id: hexMatch[0] }); + return `nostr:${nevent}`; + } catch { + return match; + } + } + // Or use bech32 if present + const bech32Match = url.match(bech32Pattern); + if (bech32Match) { + return `nostr:${bech32Match[0]}`; + } + } + // For all other links, leave the markdown link untouched + return match; + }); + + // 2. Replace bare Alexandria/localhost URLs only if they contain a Nostr identifier (not d-tag) + text = text.replace(/https?:\/\/[^\s)\]]+/g, (url) => { + if (alexandriaPattern.test(url)) { + // Ignore d-tag URLs + if (/[?&]d=/.test(url)) return url; + // Convert hexid in URL to nevent if present + const hexMatch = url.match(hexPattern); + if (hexMatch) { + try { + const nevent = nip19.neventEncode({ id: hexMatch[0] }); + return `nostr:${nevent}`; + } catch { + return url; + } + } + // Or use bech32 if present + const bech32Match = url.match(bech32Pattern); + if (bech32Match) { + return `nostr:${bech32Match[0]}`; + } + } + return url; + }); + + return text; +} + // Utility to strip tracking parameters from URLs function stripTrackingParams(url: string): string { // List of tracking params to remove @@ -41,8 +103,8 @@ function stripTrackingParams(url: string): string { } } }); - parsed.search = parsed.searchParams.toString(); - return parsed.origin + parsed.pathname + (parsed.search ? '?' + parsed.search : '') + (parsed.hash || ''); + const queryString = parsed.searchParams.toString(); + return parsed.origin + parsed.pathname + (queryString ? '?' + queryString : '') + (parsed.hash || ''); } else { // Relative URL: parse query string manually const [path, queryAndHash = ''] = url.split('?'); @@ -68,6 +130,9 @@ function processBasicFormatting(content: string): string { let processedText = content; try { + // Sanitize Alexandria Nostr links before further processing + processedText = replaceAlexandriaNostrLinks(processedText); + // Process Markdown images first processedText = processedText.replace(MARKDOWN_IMAGE, (match, alt, url) => { url = stripTrackingParams(url); diff --git a/src/lib/utils/markdown/markdownTestfile.md b/src/lib/utils/markdown/markdownTestfile.md index b7e5dbf..78176fa 100644 --- a/src/lib/utils/markdown/markdownTestfile.md +++ b/src/lib/utils/markdown/markdownTestfile.md @@ -5,6 +5,13 @@ This is a test It is _only_ a test, for __sure__. I just wanted to see if the markdown renders correctly on the page, even if I use **two asterisks** for bold text, instead of *one asterisk*.[^1] +# H1 +## H2 +### H3 +#### H4 +##### H5 +###### H6 + This file is full of ~errors~ opportunities to ~~mess up the formatting~~ check your markdown parser. npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z wrote this. That's the same person as this one with a nostr prefix nostr:npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z and nprofile1qydhwumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6tcpr3mhxue69uhkx6rjd9ehgurfd3kzumn0wd68yvfwvdhk6tcqyr7jprhgeregx7q2j4fgjmjgy0xfm34l63pqvwyf2acsd9q0mynuzp4qva3. That is a different person from npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz. @@ -92,7 +99,17 @@ https://upload.wikimedia.org/wikipedia/commons/f/f1/Heart_coraz%C3%B3n.svg This is an implementation of [Nostr-flavored Markdown](https://github.com/nostrability/nostrability/issues/146) for #gitstuff issue notes. -You can even include `code inline`, like `
      ` or +You can even turn Alexandria URLs into embedded events, if they have hexids or bech32 addresses: +http://localhost:4173/publication?id=nevent1qqstjcyerjx4laxlxc70cwzuxf3u9kkzuhdhgtu8pwrzvh7k5d5zdngpzemhxue69uhhyetvv9ujumn0wd68ytnzv9hxgq3qm3xdppkd0njmrqe2ma8a6ys39zvgp5k8u22mev8xsnqp4nh80srq0ylvuw + +But not if they have d-tags: +http://next-alexandria.gitcitadel.eu/publication?d=relay-test-thecitadel-by-unknown-v-1 + +And within a Markdown tag: [Markdown link title](http://alexandria.gitcitadel.com/publication?id=84ad65f7a321404f55d97c2208dd3686c41724e6c347d3ee53cfe16f67cdfb7c). + +And to localhost: http://localhost:4173/publication?id=c36b54991e459221f444612d88ea94ef5bb4a1b93863ef89b1328996746f6d25 + +You can even include code inline, like `
      ` or ``` in a code block diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 8ebde60..f71ac31 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -289,7 +289,7 @@
      - +
      @@ -366,7 +366,7 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi />
      {:else} -
      +
      {#key content} {#await parseAdvancedMarkdown(content)}

      Loading preview...