From 2d874af911f19eb299563e34bc7eecb91cb99c0a Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sun, 27 Oct 2024 14:31:41 -0400
Subject: [PATCH 01/23] Working summary-details, todo cleanup, newline support
 & generallayout fixes

---
 components/text.module.css |  68 ++++++++++++++++++-
 lib/rehype-sn.js           | 135 +++++++++++++++++++++++++++++++++++++
 2 files changed, 202 insertions(+), 1 deletion(-)

diff --git a/components/text.module.css b/components/text.module.css
index ff99b8a90..dac615533 100644
--- a/components/text.module.css
+++ b/components/text.module.css
@@ -432,4 +432,70 @@
     max-width: 480px;
     border-radius: 13px;
     overflow: hidden;
-}
\ No newline at end of file
+}
+
+/* Details/summary styling */
+
+.text details {
+    background-color: var(--theme-bg);
+    border: 1px solid var(--theme-borderColor);
+    border-radius: 0.4rem;
+    margin: calc(var(--grid-gap) * 0.5) 0;
+    overflow: hidden;
+}
+
+.text details[open] {
+    border-color: var(--bs-primary);
+}
+
+.text details > summary {
+    padding: 0.75rem 1rem;
+    cursor: pointer;
+    user-select: none;
+    font-weight: 500;
+    position: relative;
+}
+
+.text details > summary:hover {
+    background-color: var(--theme-commentBg);
+}
+
+.text details > summary::before {
+    content: 'ā–¶';
+    font-size: 0.8em;
+    margin-right: 0.75rem;
+    color: var(--bs-primary);
+    transition: transform 0.2s ease;
+}
+
+.text details[open] > summary::before {
+    transform: rotate(90deg);
+}
+
+.text details > *:not(summary) {
+    padding: 0.75rem 1rem;
+    color: var(--theme-grey);
+}
+
+.text details > summary + * {
+    padding-top: 0;
+}
+
+.text details > *:last-child {
+    padding-bottom: 1rem;
+}
+
+.text details .details-content {
+    padding: 0.75rem 1rem;
+    color: var(--theme-grey);
+}
+
+.text details[open] > .details-content {
+    border-top: 1px solid rgba(var(--bs-primary-rgb), 0.2);
+}
+
+.text details .details-content > br {
+    display: block;
+    content: "";
+    margin-top: 0.5rem;
+}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index fb35bf4bd..b7f1dbd94 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -16,6 +16,62 @@ export default function rehypeSN (options = {}) {
   return function transformer (tree) {
     try {
       visit(tree, (node, index, parent) => {
+        
+        // Handle raw HTML content that might be details/summary tags
+        if (node.type === 'raw') {
+          const value = node.value.trim()
+          
+          // Check if this is a details tag (single-line or multi-line)
+          if (value.includes('<details>')) {
+            const detailsNode = {
+              type: 'element',
+              tagName: 'details',
+              properties: {
+                className: ['collapsable-details']
+              },
+              children: []
+            }
+            
+            // Extract content between details tags
+            const detailsContent = value.replace(/<\/?details>/g, '').trim()
+            
+            // Check for summary tag
+            const summaryMatch = detailsContent.match(/<summary>([\s\S]*?)<\/summary>([\s\S]*)/)
+            if (summaryMatch) {
+              // Add summary element
+              detailsNode.children.push({
+                type: 'element',
+                tagName: 'summary',
+                properties: {
+                  className: ['collapsable-summary']
+                },
+                children: [{
+                  type: 'text',
+                  value: summaryMatch[1].trim()
+                }]
+              })
+              
+              // Add remaining content
+              if (summaryMatch[2].trim()) {
+                detailsNode.children.push({
+                  type: 'text',
+                  value: summaryMatch[2].trim()
+                })
+              }
+            } else {
+              // No summary found, add all content
+              detailsNode.children.push({
+                type: 'text',
+                value: detailsContent
+              })
+            }
+            
+            // Replace the raw node with our new details element
+            parent.children[index] = detailsNode
+            return [SKIP]
+          }
+        }
+
         // Handle inline code property
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
@@ -159,6 +215,85 @@ export default function rehypeSN (options = {}) {
 
         // handle custom tags
         if (node.type === 'element') {
+          // Handle details/summary tags
+          for (let i = 0; i < node.children.length; i++) {
+            const child = node.children[i]
+            
+            console.log('Checking child:', child?.type, child?.value?.trim())
+            
+            // Convert raw <details> tags into elements
+            if (child?.type === 'raw' && child.value.trim() === '<details>') {
+              console.log('Found details tag at index:', i)
+              const detailsNode = {
+                type: 'element',
+                tagName: 'details',
+                properties: {
+                  className: []  // Initialize with empty array
+                },
+                children: []
+              }
+              
+              // Collect children until we find the closing tag
+              let j = i + 1
+              while (j < node.children.length) {
+                const current = node.children[j]
+                if (current?.type === 'raw' && current.value.trim() === '</details>') {
+                  break
+                }
+                detailsNode.children.push(current)
+                j++
+              }
+              
+              // Add default summary if none exists
+              if (!detailsNode.children.some(c => 
+                (c.type === 'raw' && c.value.includes('<summary>')) || 
+                (c.type === 'element' && c.tagName === 'summary')
+              )) {
+                detailsNode.children.unshift({
+                  type: 'element',
+                  tagName: 'summary',
+                  properties: {
+                    className: []  // Initialize with empty array
+                  },
+                  children: [{ type: 'text', value: 'Details' }]
+                })
+              }
+              
+              // Convert any raw summary tags in the children 
+              for (let k = 0; k < detailsNode.children.length; k++) {
+                const child = detailsNode.children[k]
+                if (child?.type === 'raw' && child.value.trim() === '<summary>') {
+                  const summaryNode = {
+                    type: 'element',
+                    tagName: 'summary',
+                    properties: {
+                      className: []  // Initialize with empty array
+                    },
+                    children: []
+                  }
+                  
+                  // Collect summary content
+                  let l = k + 1
+                  while (l < detailsNode.children.length) {
+                    const current = detailsNode.children[l]
+                    if (current?.type === 'raw' && current.value.trim() === '</summary>') {
+                      break
+                    }
+                    summaryNode.children.push(current)
+                    l++
+                  }
+                  
+                  // Replace the raw tags and content with the summary element
+                  detailsNode.children.splice(k, l - k + 1, summaryNode)
+                }
+              }
+              
+              // Replace the original content with our new details element
+              node.children.splice(i, j - i + 1, detailsNode)
+            }
+          }
+
+          // Existing stylers handling
           for (const { startTag, endTag, className } of stylers) {
             for (let i = 0; i < node.children.length - 2; i++) {
               const [start, text, end] = node.children.slice(i, i + 3)

From dcb4e063d4d2f3bdcf0cc53686fbaa5899a1127d Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Fri, 1 Nov 2024 18:58:13 -0400
Subject: [PATCH 02/23] pre-unist-visit review and tweaks

---
 components/text.js |  3 +++
 lib/rehype-sn.js   | 45 ++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/components/text.js b/components/text.js
index fe70f0a22..913d8f364 100644
--- a/components/text.js
+++ b/components/text.js
@@ -246,3 +246,6 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
     </div>
   )
 }
+
+
+
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index b7f1dbd94..618351740 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -1,3 +1,32 @@
+/* Example of details/summary tags structures that must work as expected:
+
+<details>
+    <summary>like this</summary>
+
+lorem ipsum placeholder text
+blahblahblah
+</details>
+
+<details><summary>THE SUMMARY</summary>TESTING INSIDE DETAILS</details>
+
+<details>
+<summary>THE SUMMARY
+summary second line</summary>
+1. first things first
+2. second thing to note
+3. third thing
+</details>
+
+<details>
+  <summary>
+    THE SUMMARY
+  </summary>
+  TESTING INSIDE DETAILS
+</details> 
+*/
+
+
+
 import { SKIP, visit } from 'unist-util-visit'
 import { parseEmbedUrl, parseInternalLinks } from './url'
 import { slug } from 'github-slugger'
@@ -18,11 +47,18 @@ export default function rehypeSN (options = {}) {
       visit(tree, (node, index, parent) => {
         
         // Handle raw HTML content that might be details/summary tags
+        console.log("tree:", tree)
+        console.log("node:", node)
+        console.log("index:", index)
+        console.log("parent:", parent)
+
         if (node.type === 'raw') {
+          console.log('Checking raw node (trimmed):', node.value.trim())
           const value = node.value.trim()
           
           // Check if this is a details tag (single-line or multi-line)
           if (value.includes('<details>')) {
+            console.log('Found details tag')
             const detailsNode = {
               type: 'element',
               tagName: 'details',
@@ -34,10 +70,12 @@ export default function rehypeSN (options = {}) {
             
             // Extract content between details tags
             const detailsContent = value.replace(/<\/?details>/g, '').trim()
+            console.log('Details content:', detailsContent)
             
             // Check for summary tag
             const summaryMatch = detailsContent.match(/<summary>([\s\S]*?)<\/summary>([\s\S]*)/)
             if (summaryMatch) {
+              console.log('Found summary tag')
               // Add summary element
               detailsNode.children.push({
                 type: 'element',
@@ -50,8 +88,9 @@ export default function rehypeSN (options = {}) {
                   value: summaryMatch[1].trim()
                 }]
               })
-              
+
               // Add remaining content
+              console.log('Adding remaining content:', summaryMatch[2].trim())
               if (summaryMatch[2].trim()) {
                 detailsNode.children.push({
                   type: 'text',
@@ -60,6 +99,7 @@ export default function rehypeSN (options = {}) {
               }
             } else {
               // No summary found, add all content
+              console.log('Adding all content (no summary found, adding rest of content):', detailsContent)
               detailsNode.children.push({
                 type: 'text',
                 value: detailsContent
@@ -67,6 +107,9 @@ export default function rehypeSN (options = {}) {
             }
             
             // Replace the raw node with our new details element
+            console.log('Replacing raw node with details element')
+            console.log("raw now:", parent.children[index])
+            console.log("detailsNode (raw node will be replaced with this):", detailsNode)
             parent.children[index] = detailsNode
             return [SKIP]
           }

From b3547d1cd7156f9046d2f8c6fd46ebe09ca204ae Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sat, 9 Nov 2024 20:52:39 -0500
Subject: [PATCH 03/23] details & summary tags w functional inner markdown

---
 components/text.js         |  19 ++-
 components/text.module.css |  66 -----------
 lib/rehype-sn.js           | 233 +++++++++----------------------------
 3 files changed, 74 insertions(+), 244 deletions(-)

diff --git a/components/text.js b/components/text.js
index 913d8f364..7d2c135bc 100644
--- a/components/text.js
+++ b/components/text.js
@@ -123,7 +123,9 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
       return <Link id={props.id} target='_blank' rel={rel} href={href}>{children}</Link>
     },
     img: TextMediaOrLink,
-    embed: Embed
+    embed: Embed,
+    details: Details,
+    summary: Summary
   }), [outlawed, rel, TextMediaOrLink, topLevel])
 
   const carousel = useCarousel()
@@ -247,5 +249,18 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
   )
 }
 
+function Summary({ children }) {
+  return (
+    <summary className={styles.summary}>
+      {children}
+    </summary>
+  )
+}
 
-
+function Details({ children }) {  
+  return (
+      <details className={styles.details}>
+        {children}
+      </details>
+  )
+}
\ No newline at end of file
diff --git a/components/text.module.css b/components/text.module.css
index dac615533..a8a3c7bcd 100644
--- a/components/text.module.css
+++ b/components/text.module.css
@@ -433,69 +433,3 @@
     border-radius: 13px;
     overflow: hidden;
 }
-
-/* Details/summary styling */
-
-.text details {
-    background-color: var(--theme-bg);
-    border: 1px solid var(--theme-borderColor);
-    border-radius: 0.4rem;
-    margin: calc(var(--grid-gap) * 0.5) 0;
-    overflow: hidden;
-}
-
-.text details[open] {
-    border-color: var(--bs-primary);
-}
-
-.text details > summary {
-    padding: 0.75rem 1rem;
-    cursor: pointer;
-    user-select: none;
-    font-weight: 500;
-    position: relative;
-}
-
-.text details > summary:hover {
-    background-color: var(--theme-commentBg);
-}
-
-.text details > summary::before {
-    content: 'ā–¶';
-    font-size: 0.8em;
-    margin-right: 0.75rem;
-    color: var(--bs-primary);
-    transition: transform 0.2s ease;
-}
-
-.text details[open] > summary::before {
-    transform: rotate(90deg);
-}
-
-.text details > *:not(summary) {
-    padding: 0.75rem 1rem;
-    color: var(--theme-grey);
-}
-
-.text details > summary + * {
-    padding-top: 0;
-}
-
-.text details > *:last-child {
-    padding-bottom: 1rem;
-}
-
-.text details .details-content {
-    padding: 0.75rem 1rem;
-    color: var(--theme-grey);
-}
-
-.text details[open] > .details-content {
-    border-top: 1px solid rgba(var(--bs-primary-rgb), 0.2);
-}
-
-.text details .details-content > br {
-    display: block;
-    content: "";
-    margin-top: 0.5rem;
-}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 618351740..3f248a63b 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -1,36 +1,9 @@
-/* Example of details/summary tags structures that must work as expected:
-
-<details>
-    <summary>like this</summary>
-
-lorem ipsum placeholder text
-blahblahblah
-</details>
-
-<details><summary>THE SUMMARY</summary>TESTING INSIDE DETAILS</details>
-
-<details>
-<summary>THE SUMMARY
-summary second line</summary>
-1. first things first
-2. second thing to note
-3. third thing
-</details>
-
-<details>
-  <summary>
-    THE SUMMARY
-  </summary>
-  TESTING INSIDE DETAILS
-</details> 
-*/
-
-
-
 import { SKIP, visit } from 'unist-util-visit'
 import { parseEmbedUrl, parseInternalLinks } from './url'
 import { slug } from 'github-slugger'
 import { toString } from 'mdast-util-to-string'
+import { fromMarkdown } from 'mdast-util-from-markdown'
+import { toHast } from 'mdast-util-to-hast'
 
 const userGroup = '[\\w_]+'
 const subGroup = '[A-Za-z][\\w_]+'
@@ -45,75 +18,6 @@ export default function rehypeSN (options = {}) {
   return function transformer (tree) {
     try {
       visit(tree, (node, index, parent) => {
-        
-        // Handle raw HTML content that might be details/summary tags
-        console.log("tree:", tree)
-        console.log("node:", node)
-        console.log("index:", index)
-        console.log("parent:", parent)
-
-        if (node.type === 'raw') {
-          console.log('Checking raw node (trimmed):', node.value.trim())
-          const value = node.value.trim()
-          
-          // Check if this is a details tag (single-line or multi-line)
-          if (value.includes('<details>')) {
-            console.log('Found details tag')
-            const detailsNode = {
-              type: 'element',
-              tagName: 'details',
-              properties: {
-                className: ['collapsable-details']
-              },
-              children: []
-            }
-            
-            // Extract content between details tags
-            const detailsContent = value.replace(/<\/?details>/g, '').trim()
-            console.log('Details content:', detailsContent)
-            
-            // Check for summary tag
-            const summaryMatch = detailsContent.match(/<summary>([\s\S]*?)<\/summary>([\s\S]*)/)
-            if (summaryMatch) {
-              console.log('Found summary tag')
-              // Add summary element
-              detailsNode.children.push({
-                type: 'element',
-                tagName: 'summary',
-                properties: {
-                  className: ['collapsable-summary']
-                },
-                children: [{
-                  type: 'text',
-                  value: summaryMatch[1].trim()
-                }]
-              })
-
-              // Add remaining content
-              console.log('Adding remaining content:', summaryMatch[2].trim())
-              if (summaryMatch[2].trim()) {
-                detailsNode.children.push({
-                  type: 'text',
-                  value: summaryMatch[2].trim()
-                })
-              }
-            } else {
-              // No summary found, add all content
-              console.log('Adding all content (no summary found, adding rest of content):', detailsContent)
-              detailsNode.children.push({
-                type: 'text',
-                value: detailsContent
-              })
-            }
-            
-            // Replace the raw node with our new details element
-            console.log('Replacing raw node with details element')
-            console.log("raw now:", parent.children[index])
-            console.log("detailsNode (raw node will be replaced with this):", detailsNode)
-            parent.children[index] = detailsNode
-            return [SKIP]
-          }
-        }
 
         // Handle inline code property
         if (node.tagName === 'code') {
@@ -258,84 +162,6 @@ export default function rehypeSN (options = {}) {
 
         // handle custom tags
         if (node.type === 'element') {
-          // Handle details/summary tags
-          for (let i = 0; i < node.children.length; i++) {
-            const child = node.children[i]
-            
-            console.log('Checking child:', child?.type, child?.value?.trim())
-            
-            // Convert raw <details> tags into elements
-            if (child?.type === 'raw' && child.value.trim() === '<details>') {
-              console.log('Found details tag at index:', i)
-              const detailsNode = {
-                type: 'element',
-                tagName: 'details',
-                properties: {
-                  className: []  // Initialize with empty array
-                },
-                children: []
-              }
-              
-              // Collect children until we find the closing tag
-              let j = i + 1
-              while (j < node.children.length) {
-                const current = node.children[j]
-                if (current?.type === 'raw' && current.value.trim() === '</details>') {
-                  break
-                }
-                detailsNode.children.push(current)
-                j++
-              }
-              
-              // Add default summary if none exists
-              if (!detailsNode.children.some(c => 
-                (c.type === 'raw' && c.value.includes('<summary>')) || 
-                (c.type === 'element' && c.tagName === 'summary')
-              )) {
-                detailsNode.children.unshift({
-                  type: 'element',
-                  tagName: 'summary',
-                  properties: {
-                    className: []  // Initialize with empty array
-                  },
-                  children: [{ type: 'text', value: 'Details' }]
-                })
-              }
-              
-              // Convert any raw summary tags in the children 
-              for (let k = 0; k < detailsNode.children.length; k++) {
-                const child = detailsNode.children[k]
-                if (child?.type === 'raw' && child.value.trim() === '<summary>') {
-                  const summaryNode = {
-                    type: 'element',
-                    tagName: 'summary',
-                    properties: {
-                      className: []  // Initialize with empty array
-                    },
-                    children: []
-                  }
-                  
-                  // Collect summary content
-                  let l = k + 1
-                  while (l < detailsNode.children.length) {
-                    const current = detailsNode.children[l]
-                    if (current?.type === 'raw' && current.value.trim() === '</summary>') {
-                      break
-                    }
-                    summaryNode.children.push(current)
-                    l++
-                  }
-                  
-                  // Replace the raw tags and content with the summary element
-                  detailsNode.children.splice(k, l - k + 1, summaryNode)
-                }
-              }
-              
-              // Replace the original content with our new details element
-              node.children.splice(i, j - i + 1, detailsNode)
-            }
-          }
-
           // Existing stylers handling
           for (const { startTag, endTag, className } of stylers) {
             for (let i = 0; i < node.children.length - 2; i++) {
@@ -392,6 +218,61 @@ export default function rehypeSN (options = {}) {
             return index + 1
           }
         }
+
+        // Handle raw HTML content for details/summary tags
+        if (node.type === 'raw') {
+          const value = node.value.trim()
+          
+          if (value.includes('<details>')) {
+            // Step 1: Extract everything between details tags
+            const detailsRegex = /<details>([\s\S]*?)<\/details>/
+            const detailsMatch = value.match(detailsRegex)
+            if (!detailsMatch) return
+            
+            const fullContent = detailsMatch[1]
+            
+            // Step 2: Extract summary content
+            const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
+            const summaryMatch = fullContent.match(summaryRegex)
+            if (!summaryMatch) return
+            
+            const summaryContent = summaryMatch[1].trim()
+            
+            // Step 3: Get remaining content
+            const remainingContent = fullContent
+              .replace(summaryMatch[0], '')  // Remove summary tag and its content
+              .trim()
+            
+            // Step 4: Process remaining content as markdown
+            const mdast = fromMarkdown(remainingContent)
+            const contentHast = toHast(mdast)
+            
+            // Step 5: Construct new node structure with proper HAST nodes
+            const detailsNode = {
+              type: 'element',
+              tagName: 'details',
+              properties: {},
+              children: [
+                // Summary node
+                {
+                  type: 'element',
+                  tagName: 'summary',
+                  properties: {},
+                  children: [{
+                    type: 'text',
+                    value: summaryContent
+                  }]
+                },
+                // Content node(s) - spread all children from processed markdown
+                ...(contentHast?.children || [])
+              ]
+            }
+            
+            // Step 6: Replace original node with our new structure
+            parent.children[index] = detailsNode
+            return [SKIP]
+          }
+        }
       })
     } catch (error) {
       console.error('Error in rehypeSN transformer:', error)

From 004c9477d50e6dbfcecee29e8562b510ab180043 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sat, 9 Nov 2024 21:25:11 -0500
Subject: [PATCH 04/23] Clean up comments and improve readability

---
 lib/rehype-sn.js | 55 ++++++++++++++++++++++++------------------------
 1 file changed, 28 insertions(+), 27 deletions(-)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 3f248a63b..5e4fd503f 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -11,6 +11,8 @@ const subGroup = '[A-Za-z][\\w_]+'
 const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
+const detailsRegex = /<details>([\s\S]*?)<\/details>/
+const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
 
 export default function rehypeSN (options = {}) {
   const { stylers = [] } = options
@@ -219,56 +221,55 @@ export default function rehypeSN (options = {}) {
           }
         }
 
-        // Handle raw HTML content for details/summary tags
+        // Handle details/summary tags
         if (node.type === 'raw') {
           const value = node.value.trim()
           
           if (value.includes('<details>')) {
-            // Step 1: Extract everything between details tags
-            const detailsRegex = /<details>([\s\S]*?)<\/details>/
+            // Extract content between details tags
             const detailsMatch = value.match(detailsRegex)
             if (!detailsMatch) return
             
             const fullContent = detailsMatch[1]
             
-            // Step 2: Extract summary content
-            const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
+            // Try to extract summary content if it exists
             const summaryMatch = fullContent.match(summaryRegex)
-            if (!summaryMatch) return
+            const summaryContent = summaryMatch 
+              ? summaryMatch[1].trim()
+              : 'Details' // Default summary text if none provided
             
-            const summaryContent = summaryMatch[1].trim()
+            // Get remaining content, accounting for optional summary
+            const remainingContent = summaryMatch
+              ? fullContent.replace(summaryMatch[0], '').trim()
+              : fullContent.trim()
             
-            // Step 3: Get remaining content
-            const remainingContent = fullContent
-              .replace(summaryMatch[0], '')  // Remove summary tag and its content
-              .trim()
-            
-            // Step 4: Process remaining content as markdown
+            // Convert markdown content to HTML AST nodes
+            // This allows proper rendering of markdown syntax like
+            // **bold**, *italic*, lists, etc. within details tags
             const mdast = fromMarkdown(remainingContent)
             const contentHast = toHast(mdast)
             
-            // Step 5: Construct new node structure with proper HAST nodes
+            const summaryNode = {
+              type: 'element',
+              tagName: 'summary',
+              properties: {},
+              children: [{
+                type: 'text',
+                value: summaryContent
+              }]
+            }
+            
             const detailsNode = {
               type: 'element',
               tagName: 'details',
               properties: {},
               children: [
-                // Summary node
-                {
-                  type: 'element',
-                  tagName: 'summary',
-                  properties: {},
-                  children: [{
-                    type: 'text',
-                    value: summaryContent
-                  }]
-                },
-                // Content node(s) - spread all children from processed markdown
-                ...(contentHast?.children || [])
+                summaryNode,
+                ...(contentHast?.children ?? [])
               ]
             }
             
-            // Step 6: Replace original node with our new structure
+            // Replace original node with new structure
             parent.children[index] = detailsNode
             return [SKIP]
           }

From 71db2f9b9f48c96d8682696e652adabac3d0ec3b Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sat, 9 Nov 2024 22:53:38 -0500
Subject: [PATCH 05/23] Lint fixes

---
 components/text.js | 12 ++++++------
 lib/rehype-sn.js   | 24 ++++++++++--------------
 2 files changed, 16 insertions(+), 20 deletions(-)

diff --git a/components/text.js b/components/text.js
index 7d2c135bc..b4683ad76 100644
--- a/components/text.js
+++ b/components/text.js
@@ -249,7 +249,7 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
   )
 }
 
-function Summary({ children }) {
+function Summary ({ children }) {
   return (
     <summary className={styles.summary}>
       {children}
@@ -257,10 +257,10 @@ function Summary({ children }) {
   )
 }
 
-function Details({ children }) {  
+function Details ({ children }) {
   return (
-      <details className={styles.details}>
-        {children}
-      </details>
+    <details className={styles.details}>
+      {children}
+    </details>
   )
-}
\ No newline at end of file
+}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 5e4fd503f..73afc5445 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -20,18 +20,15 @@ export default function rehypeSN (options = {}) {
   return function transformer (tree) {
     try {
       visit(tree, (node, index, parent) => {
-
         // Handle inline code property
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
         }
-
         // handle headings
         if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
           const nodeText = toString(node)
           const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
           node.properties.id = headingId
-
           // Create a new link element
           const linkElement = {
             type: 'element',
@@ -41,7 +38,6 @@ export default function rehypeSN (options = {}) {
             },
             children: [{ type: 'text', value: nodeText }]
           }
-
           // Replace the heading's children with the new link element
           node.children = [linkElement]
           return [SKIP]
@@ -224,31 +220,31 @@ export default function rehypeSN (options = {}) {
         // Handle details/summary tags
         if (node.type === 'raw') {
           const value = node.value.trim()
-          
+
           if (value.includes('<details>')) {
-            // Extract content between details tags
+          // Extract content between details tags
             const detailsMatch = value.match(detailsRegex)
             if (!detailsMatch) return
-            
+
             const fullContent = detailsMatch[1]
-            
+
             // Try to extract summary content if it exists
             const summaryMatch = fullContent.match(summaryRegex)
-            const summaryContent = summaryMatch 
+            const summaryContent = summaryMatch
               ? summaryMatch[1].trim()
               : 'Details' // Default summary text if none provided
-            
+
             // Get remaining content, accounting for optional summary
             const remainingContent = summaryMatch
               ? fullContent.replace(summaryMatch[0], '').trim()
               : fullContent.trim()
-            
+
             // Convert markdown content to HTML AST nodes
             // This allows proper rendering of markdown syntax like
             // **bold**, *italic*, lists, etc. within details tags
             const mdast = fromMarkdown(remainingContent)
             const contentHast = toHast(mdast)
-            
+
             const summaryNode = {
               type: 'element',
               tagName: 'summary',
@@ -258,7 +254,7 @@ export default function rehypeSN (options = {}) {
                 value: summaryContent
               }]
             }
-            
+
             const detailsNode = {
               type: 'element',
               tagName: 'details',
@@ -268,7 +264,7 @@ export default function rehypeSN (options = {}) {
                 ...(contentHast?.children ?? [])
               ]
             }
-            
+
             // Replace original node with new structure
             parent.children[index] = detailsNode
             return [SKIP]

From 8d068428e71851497462e409504038cc3bcd119e Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sun, 17 Nov 2024 02:30:12 -0500
Subject: [PATCH 06/23] Handle blank lines, prevent some nodes breaking details
 tag

---
 lib/rehype-sn.js | 320 +++++++++++++----------------------------------
 1 file changed, 86 insertions(+), 234 deletions(-)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 73afc5445..e1a6bfec6 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -11,7 +11,7 @@ const subGroup = '[A-Za-z][\\w_]+'
 const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
-const detailsRegex = /<details>([\s\S]*?)<\/details>/
+const detailsRegex = /<details>\s*([\s\S]*?)\s*<\/details>/
 const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
 
 export default function rehypeSN (options = {}) {
@@ -19,254 +19,99 @@ export default function rehypeSN (options = {}) {
 
   return function transformer (tree) {
     try {
+      // First pass: find and combine split details tags
+      // This handles cases where blank lines cause the parser to split nodes
       visit(tree, (node, index, parent) => {
-        // Handle inline code property
-        if (node.tagName === 'code') {
-          node.properties.inline = !(parent && parent.tagName === 'pre')
-        }
-        // handle headings
-        if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
-          const nodeText = toString(node)
-          const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
-          node.properties.id = headingId
-          // Create a new link element
-          const linkElement = {
-            type: 'element',
-            tagName: 'headlink',
-            properties: {
-              href: `#${headingId}`
-            },
-            children: [{ type: 'text', value: nodeText }]
-          }
-          // Replace the heading's children with the new link element
-          node.children = [linkElement]
-          return [SKIP]
-        }
-
-        // if img is wrapped in a link, remove the link
-        if (node.tagName === 'a' && node.children.length === 1 && node.children[0].tagName === 'img') {
-          parent.children[index] = node.children[0]
-          return index
-        }
-
-        // handle internal links
-        if (node.tagName === 'a') {
-          try {
-            if (node.properties.href.includes('#itemfn-')) {
-              node.tagName = 'footnote'
-            } else {
-              const { itemId, linkText } = parseInternalLinks(node.properties.href)
-              if (itemId) {
-                node.tagName = 'item'
-                node.properties.id = itemId
-                if (node.properties.href === toString(node)) {
-                  node.children[0].value = linkText
-                }
-              }
-            }
-          } catch {
-            // ignore errors like invalid URLs
-          }
-        }
-
-        // only show a link as an embed if it doesn't have text siblings
-        if (node.tagName === 'a' &&
-                  !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
-                  toString(node) === node.properties.href) {
-          const embed = parseEmbedUrl(node.properties.href)
-          if (embed) {
-            node.tagName = 'embed'
-            node.properties = { ...embed, src: node.properties.href }
-          } else {
-            node.tagName = 'autolink'
-          }
-        }
-
-        // if the link text is a URL, just show the URL
-        if (node.tagName === 'a' && isMisleadingLink(toString(node), node.properties.href)) {
-          node.children = [{ type: 'text', value: node.properties.href }]
-          return [SKIP]
-        }
-
-        // Handle @mentions and ~subs
-        if (node.type === 'text') {
-          const newChildren = []
-          let lastIndex = 0
-          let match
-          let childrenConsumed = 1
-          let text = toString(node)
-
-          const combinedRegex = new RegExp(mentionRegex.source + '|' + subRegex.source, 'gi')
-
-          // handle @__username__ or ~__sub__
-          if (['@', '~'].includes(node.value) &&
-            parent.children[index + 1]?.tagName === 'strong' &&
-            parent.children[index + 1].children[0]?.type === 'text') {
-            childrenConsumed = 2
-            text = node.value + '__' + toString(parent.children[index + 1]) + '__'
-          }
-
-          while ((match = combinedRegex.exec(text)) !== null) {
-            if (lastIndex < match.index) {
-              newChildren.push({ type: 'text', value: text.slice(lastIndex, match.index) })
-            }
-
-            const [fullMatch, mentionMatch, subMatch] = match
-            const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch) : replaceSub(fullMatch, subMatch)
-
-            if (replacement) {
-              newChildren.push(replacement)
-            } else {
-              newChildren.push({ type: 'text', value: fullMatch })
-            }
-
-            lastIndex = combinedRegex.lastIndex
-          }
-
-          if (newChildren.length > 0) {
-            if (lastIndex < text.length) {
-              newChildren.push({ type: 'text', value: text.slice(lastIndex) })
-            }
-            parent.children.splice(index, childrenConsumed, ...newChildren)
-            return index + newChildren.length
-          }
-        }
-
-        // Handle Nostr IDs
-        if (node.type === 'text') {
-          const newChildren = []
-          let lastIndex = 0
-          let match
-
-          while ((match = nostrIdRegex.exec(node.value)) !== null) {
-            if (lastIndex < match.index) {
-              newChildren.push({ type: 'text', value: node.value.slice(lastIndex, match.index) })
-            }
-
-            newChildren.push(replaceNostrId(match[0], match[0]))
-
-            lastIndex = nostrIdRegex.lastIndex
-          }
-
-          if (lastIndex < node.value.length) {
-            newChildren.push({ type: 'text', value: node.value.slice(lastIndex) })
-          }
-
-          if (newChildren.length > 0) {
-            parent.children.splice(index, 1, ...newChildren)
-            return index + newChildren.length
-          }
-        }
-
-        // handle custom tags
-        if (node.type === 'element') {
-          // Existing stylers handling
-          for (const { startTag, endTag, className } of stylers) {
-            for (let i = 0; i < node.children.length - 2; i++) {
-              const [start, text, end] = node.children.slice(i, i + 3)
-
-              if (start?.type === 'raw' && start?.value === startTag &&
-                  text?.type === 'text' &&
-                  end?.type === 'raw' && end?.value === endTag) {
-                const newChild = {
-                  type: 'element',
-                  tagName: 'span',
-                  properties: { className: [className] },
-                  children: [{ type: 'text', value: text.value }]
+        if (node.type === 'raw' && parent?.children) {
+          // Look for details tags that are incomplete (no closing tag)
+          if (node.value.includes('<details') && !node.value.includes('</details>')) {
+            let nextIndex = index + 1
+            let content = node.value
+
+            // Keep looking through subsequent nodes until we find the closing tag
+            while (nextIndex < parent.children.length) {
+              const nextNode = parent.children[nextIndex]
+              
+              // Handle different types of content nodes
+              if (nextNode.type === 'raw') {
+                content += nextNode.value
+                // Found the closing tag - combine all nodes and stop
+                if (nextNode.value.includes('</details>')) {
+                  node.value = content
+                  // Remove the now-combined nodes
+                  parent.children.splice(index + 1, nextIndex - index)
+                  return [SKIP]
                 }
-                node.children.splice(i, 3, newChild)
+              } else if (nextNode.type === 'text' || nextNode.type === 'element') {
+                // Preserve content from text nodes and elements
+                content += getTextContent(nextNode)
               }
-            }
-          }
-        }
-
-        // merge adjacent images and empty paragraphs into a single image collage
-        if ((node.tagName === 'img' || isImageOnlyParagraph(node)) && Array.isArray(parent.children)) {
-          const adjacentNodes = [node]
-          let nextIndex = index + 1
-          const siblings = parent.children
-          const somethingBefore = parent.children[index - 1] && parent.children[index - 1].tagName !== 'p'
-          let somethingAfter = false
-
-          while (nextIndex < siblings.length) {
-            const nextNode = siblings[nextIndex]
-            if (!nextNode) break
-            if (nextNode.tagName === 'img' || isImageOnlyParagraph(nextNode)) {
-              adjacentNodes.push(nextNode)
-              nextIndex++
-            } else if (nextNode.type === 'text' && typeof nextNode.value === 'string' && !nextNode.value.trim()) {
               nextIndex++
-            } else {
-              somethingAfter = true
-              break
-            }
-          }
-
-          if (adjacentNodes.length > 0) {
-            const allImages = adjacentNodes.flatMap(n =>
-              n.tagName === 'img' ? [n] : (Array.isArray(n.children) ? n.children.filter(child => child.tagName === 'img') : [])
-            )
-            const collageNode = {
-              type: 'element',
-              tagName: 'p',
-              children: allImages,
-              properties: { onlyImages: true, somethingBefore, somethingAfter }
             }
-            parent.children.splice(index, nextIndex - index, collageNode)
-            return index + 1
           }
         }
+      })
 
-        // Handle details/summary tags
+      // Second pass: process details tags into proper HTML structure
+      visit(tree, (node) => {
         if (node.type === 'raw') {
           const value = node.value.trim()
 
+          // Only process nodes containing complete details tags
           if (value.includes('<details>')) {
-          // Extract content between details tags
-            const detailsMatch = value.match(detailsRegex)
-            if (!detailsMatch) return
-
-            const fullContent = detailsMatch[1]
-
-            // Try to extract summary content if it exists
-            const summaryMatch = fullContent.match(summaryRegex)
-            const summaryContent = summaryMatch
-              ? summaryMatch[1].trim()
-              : 'Details' // Default summary text if none provided
-
-            // Get remaining content, accounting for optional summary
-            const remainingContent = summaryMatch
-              ? fullContent.replace(summaryMatch[0], '').trim()
-              : fullContent.trim()
-
-            // Convert markdown content to HTML AST nodes
-            // This allows proper rendering of markdown syntax like
-            // **bold**, *italic*, lists, etc. within details tags
-            const mdast = fromMarkdown(remainingContent)
-            const contentHast = toHast(mdast)
+            // Find all details blocks in the content
+            const detailsMatches = Array.from(value.matchAll(/<details>\s*([\s\S]*?)\s*<\/details>/g))
+            if (!detailsMatches.length) return
+
+            detailsMatches.forEach(match => {
+              const content = match[1]
+              
+              // Extract summary content if present, or use default
+              const summaryMatch = content.match(/<summary>([\s\S]*?)<\/summary>/)
+              const summaryContent = summaryMatch
+                ? summaryMatch[1].trim()
+                : 'Details'
+
+              // Get remaining content after summary
+              let remainingContent = summaryMatch
+                ? content.replace(summaryMatch[0], '').trim()
+                : content.trim()
+
+              // Normalize newlines for consistent rendering
+              remainingContent = remainingContent
+                .replace(/\r\n/g, '\n')
+                .replace(/\n\s*\n/g, '\n\n')
+                .trim()
+
+              // Convert markdown content to HTML structure
+              const mdast = fromMarkdown(remainingContent)
+              const contentHast = toHast(mdast)
+
+              // Create the details node structure
+              const summaryNode = {
+                type: 'element',
+                tagName: 'summary',
+                properties: {},
+                children: [{
+                  type: 'text',
+                  value: summaryContent
+                }]
+              }
 
-            const summaryNode = {
-              type: 'element',
-              tagName: 'summary',
-              properties: {},
-              children: [{
-                type: 'text',
-                value: summaryContent
-              }]
-            }
+              const detailsNode = {
+                type: 'element',
+                tagName: 'details',
+                properties: {},
+                children: [
+                  summaryNode,
+                  ...(contentHast?.children ?? [])
+                ]
+              }
 
-            const detailsNode = {
-              type: 'element',
-              tagName: 'details',
-              properties: {},
-              children: [
-                summaryNode,
-                ...(contentHast?.children ?? [])
-              ]
-            }
+              // Replace the original node with our structured version
+              Object.assign(node, detailsNode)
+            })
 
-            // Replace original node with new structure
-            parent.children[index] = detailsNode
             return [SKIP]
           }
         }
@@ -331,3 +176,10 @@ export default function rehypeSN (options = {}) {
     }
   }
 }
+
+// Helper function to extract text content from nodes
+function getTextContent(node) {
+  if (node.value) return node.value
+  if (!node.children) return ''
+  return node.children.map(child => getTextContent(child)).join('')
+}

From 8ff93734abf351d5612fa2ac8e79f067a5ce3242 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sun, 17 Nov 2024 02:33:03 -0500
Subject: [PATCH 07/23] Revert "Handle blank lines, prevent some nodes breaking
 details tag"

This reverts commit 8d068428e71851497462e409504038cc3bcd119e.
---
 lib/rehype-sn.js | 320 ++++++++++++++++++++++++++++++++++-------------
 1 file changed, 234 insertions(+), 86 deletions(-)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index e1a6bfec6..73afc5445 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -11,7 +11,7 @@ const subGroup = '[A-Za-z][\\w_]+'
 const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
-const detailsRegex = /<details>\s*([\s\S]*?)\s*<\/details>/
+const detailsRegex = /<details>([\s\S]*?)<\/details>/
 const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
 
 export default function rehypeSN (options = {}) {
@@ -19,99 +19,254 @@ export default function rehypeSN (options = {}) {
 
   return function transformer (tree) {
     try {
-      // First pass: find and combine split details tags
-      // This handles cases where blank lines cause the parser to split nodes
       visit(tree, (node, index, parent) => {
-        if (node.type === 'raw' && parent?.children) {
-          // Look for details tags that are incomplete (no closing tag)
-          if (node.value.includes('<details') && !node.value.includes('</details>')) {
-            let nextIndex = index + 1
-            let content = node.value
-
-            // Keep looking through subsequent nodes until we find the closing tag
-            while (nextIndex < parent.children.length) {
-              const nextNode = parent.children[nextIndex]
-              
-              // Handle different types of content nodes
-              if (nextNode.type === 'raw') {
-                content += nextNode.value
-                // Found the closing tag - combine all nodes and stop
-                if (nextNode.value.includes('</details>')) {
-                  node.value = content
-                  // Remove the now-combined nodes
-                  parent.children.splice(index + 1, nextIndex - index)
-                  return [SKIP]
+        // Handle inline code property
+        if (node.tagName === 'code') {
+          node.properties.inline = !(parent && parent.tagName === 'pre')
+        }
+        // handle headings
+        if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
+          const nodeText = toString(node)
+          const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
+          node.properties.id = headingId
+          // Create a new link element
+          const linkElement = {
+            type: 'element',
+            tagName: 'headlink',
+            properties: {
+              href: `#${headingId}`
+            },
+            children: [{ type: 'text', value: nodeText }]
+          }
+          // Replace the heading's children with the new link element
+          node.children = [linkElement]
+          return [SKIP]
+        }
+
+        // if img is wrapped in a link, remove the link
+        if (node.tagName === 'a' && node.children.length === 1 && node.children[0].tagName === 'img') {
+          parent.children[index] = node.children[0]
+          return index
+        }
+
+        // handle internal links
+        if (node.tagName === 'a') {
+          try {
+            if (node.properties.href.includes('#itemfn-')) {
+              node.tagName = 'footnote'
+            } else {
+              const { itemId, linkText } = parseInternalLinks(node.properties.href)
+              if (itemId) {
+                node.tagName = 'item'
+                node.properties.id = itemId
+                if (node.properties.href === toString(node)) {
+                  node.children[0].value = linkText
+                }
+              }
+            }
+          } catch {
+            // ignore errors like invalid URLs
+          }
+        }
+
+        // only show a link as an embed if it doesn't have text siblings
+        if (node.tagName === 'a' &&
+                  !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
+                  toString(node) === node.properties.href) {
+          const embed = parseEmbedUrl(node.properties.href)
+          if (embed) {
+            node.tagName = 'embed'
+            node.properties = { ...embed, src: node.properties.href }
+          } else {
+            node.tagName = 'autolink'
+          }
+        }
+
+        // if the link text is a URL, just show the URL
+        if (node.tagName === 'a' && isMisleadingLink(toString(node), node.properties.href)) {
+          node.children = [{ type: 'text', value: node.properties.href }]
+          return [SKIP]
+        }
+
+        // Handle @mentions and ~subs
+        if (node.type === 'text') {
+          const newChildren = []
+          let lastIndex = 0
+          let match
+          let childrenConsumed = 1
+          let text = toString(node)
+
+          const combinedRegex = new RegExp(mentionRegex.source + '|' + subRegex.source, 'gi')
+
+          // handle @__username__ or ~__sub__
+          if (['@', '~'].includes(node.value) &&
+            parent.children[index + 1]?.tagName === 'strong' &&
+            parent.children[index + 1].children[0]?.type === 'text') {
+            childrenConsumed = 2
+            text = node.value + '__' + toString(parent.children[index + 1]) + '__'
+          }
+
+          while ((match = combinedRegex.exec(text)) !== null) {
+            if (lastIndex < match.index) {
+              newChildren.push({ type: 'text', value: text.slice(lastIndex, match.index) })
+            }
+
+            const [fullMatch, mentionMatch, subMatch] = match
+            const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch) : replaceSub(fullMatch, subMatch)
+
+            if (replacement) {
+              newChildren.push(replacement)
+            } else {
+              newChildren.push({ type: 'text', value: fullMatch })
+            }
+
+            lastIndex = combinedRegex.lastIndex
+          }
+
+          if (newChildren.length > 0) {
+            if (lastIndex < text.length) {
+              newChildren.push({ type: 'text', value: text.slice(lastIndex) })
+            }
+            parent.children.splice(index, childrenConsumed, ...newChildren)
+            return index + newChildren.length
+          }
+        }
+
+        // Handle Nostr IDs
+        if (node.type === 'text') {
+          const newChildren = []
+          let lastIndex = 0
+          let match
+
+          while ((match = nostrIdRegex.exec(node.value)) !== null) {
+            if (lastIndex < match.index) {
+              newChildren.push({ type: 'text', value: node.value.slice(lastIndex, match.index) })
+            }
+
+            newChildren.push(replaceNostrId(match[0], match[0]))
+
+            lastIndex = nostrIdRegex.lastIndex
+          }
+
+          if (lastIndex < node.value.length) {
+            newChildren.push({ type: 'text', value: node.value.slice(lastIndex) })
+          }
+
+          if (newChildren.length > 0) {
+            parent.children.splice(index, 1, ...newChildren)
+            return index + newChildren.length
+          }
+        }
+
+        // handle custom tags
+        if (node.type === 'element') {
+          // Existing stylers handling
+          for (const { startTag, endTag, className } of stylers) {
+            for (let i = 0; i < node.children.length - 2; i++) {
+              const [start, text, end] = node.children.slice(i, i + 3)
+
+              if (start?.type === 'raw' && start?.value === startTag &&
+                  text?.type === 'text' &&
+                  end?.type === 'raw' && end?.value === endTag) {
+                const newChild = {
+                  type: 'element',
+                  tagName: 'span',
+                  properties: { className: [className] },
+                  children: [{ type: 'text', value: text.value }]
                 }
-              } else if (nextNode.type === 'text' || nextNode.type === 'element') {
-                // Preserve content from text nodes and elements
-                content += getTextContent(nextNode)
+                node.children.splice(i, 3, newChild)
               }
+            }
+          }
+        }
+
+        // merge adjacent images and empty paragraphs into a single image collage
+        if ((node.tagName === 'img' || isImageOnlyParagraph(node)) && Array.isArray(parent.children)) {
+          const adjacentNodes = [node]
+          let nextIndex = index + 1
+          const siblings = parent.children
+          const somethingBefore = parent.children[index - 1] && parent.children[index - 1].tagName !== 'p'
+          let somethingAfter = false
+
+          while (nextIndex < siblings.length) {
+            const nextNode = siblings[nextIndex]
+            if (!nextNode) break
+            if (nextNode.tagName === 'img' || isImageOnlyParagraph(nextNode)) {
+              adjacentNodes.push(nextNode)
+              nextIndex++
+            } else if (nextNode.type === 'text' && typeof nextNode.value === 'string' && !nextNode.value.trim()) {
               nextIndex++
+            } else {
+              somethingAfter = true
+              break
+            }
+          }
+
+          if (adjacentNodes.length > 0) {
+            const allImages = adjacentNodes.flatMap(n =>
+              n.tagName === 'img' ? [n] : (Array.isArray(n.children) ? n.children.filter(child => child.tagName === 'img') : [])
+            )
+            const collageNode = {
+              type: 'element',
+              tagName: 'p',
+              children: allImages,
+              properties: { onlyImages: true, somethingBefore, somethingAfter }
             }
+            parent.children.splice(index, nextIndex - index, collageNode)
+            return index + 1
           }
         }
-      })
 
-      // Second pass: process details tags into proper HTML structure
-      visit(tree, (node) => {
+        // Handle details/summary tags
         if (node.type === 'raw') {
           const value = node.value.trim()
 
-          // Only process nodes containing complete details tags
           if (value.includes('<details>')) {
-            // Find all details blocks in the content
-            const detailsMatches = Array.from(value.matchAll(/<details>\s*([\s\S]*?)\s*<\/details>/g))
-            if (!detailsMatches.length) return
-
-            detailsMatches.forEach(match => {
-              const content = match[1]
-              
-              // Extract summary content if present, or use default
-              const summaryMatch = content.match(/<summary>([\s\S]*?)<\/summary>/)
-              const summaryContent = summaryMatch
-                ? summaryMatch[1].trim()
-                : 'Details'
-
-              // Get remaining content after summary
-              let remainingContent = summaryMatch
-                ? content.replace(summaryMatch[0], '').trim()
-                : content.trim()
-
-              // Normalize newlines for consistent rendering
-              remainingContent = remainingContent
-                .replace(/\r\n/g, '\n')
-                .replace(/\n\s*\n/g, '\n\n')
-                .trim()
-
-              // Convert markdown content to HTML structure
-              const mdast = fromMarkdown(remainingContent)
-              const contentHast = toHast(mdast)
-
-              // Create the details node structure
-              const summaryNode = {
-                type: 'element',
-                tagName: 'summary',
-                properties: {},
-                children: [{
-                  type: 'text',
-                  value: summaryContent
-                }]
-              }
+          // Extract content between details tags
+            const detailsMatch = value.match(detailsRegex)
+            if (!detailsMatch) return
 
-              const detailsNode = {
-                type: 'element',
-                tagName: 'details',
-                properties: {},
-                children: [
-                  summaryNode,
-                  ...(contentHast?.children ?? [])
-                ]
-              }
+            const fullContent = detailsMatch[1]
 
-              // Replace the original node with our structured version
-              Object.assign(node, detailsNode)
-            })
+            // Try to extract summary content if it exists
+            const summaryMatch = fullContent.match(summaryRegex)
+            const summaryContent = summaryMatch
+              ? summaryMatch[1].trim()
+              : 'Details' // Default summary text if none provided
 
+            // Get remaining content, accounting for optional summary
+            const remainingContent = summaryMatch
+              ? fullContent.replace(summaryMatch[0], '').trim()
+              : fullContent.trim()
+
+            // Convert markdown content to HTML AST nodes
+            // This allows proper rendering of markdown syntax like
+            // **bold**, *italic*, lists, etc. within details tags
+            const mdast = fromMarkdown(remainingContent)
+            const contentHast = toHast(mdast)
+
+            const summaryNode = {
+              type: 'element',
+              tagName: 'summary',
+              properties: {},
+              children: [{
+                type: 'text',
+                value: summaryContent
+              }]
+            }
+
+            const detailsNode = {
+              type: 'element',
+              tagName: 'details',
+              properties: {},
+              children: [
+                summaryNode,
+                ...(contentHast?.children ?? [])
+              ]
+            }
+
+            // Replace original node with new structure
+            parent.children[index] = detailsNode
             return [SKIP]
           }
         }
@@ -176,10 +331,3 @@ export default function rehypeSN (options = {}) {
     }
   }
 }
-
-// Helper function to extract text content from nodes
-function getTextContent(node) {
-  if (node.value) return node.value
-  if (!node.children) return ''
-  return node.children.map(child => getTextContent(child)).join('')
-}

From 9c75beb9521bf020c0973f4f7f44cc8a2af9d365 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sun, 17 Nov 2024 03:12:27 -0500
Subject: [PATCH 08/23] cleanup

---
 components/text.js         | 20 +++++++++++-
 components/text.module.css |  2 +-
 lib/rehype-sn.js           | 62 ++++++++++++++++++++++++++++++++++++--
 3 files changed, 79 insertions(+), 5 deletions(-)

diff --git a/components/text.js b/components/text.js
index 43b9a387f..52b8b959b 100644
--- a/components/text.js
+++ b/components/text.js
@@ -124,7 +124,9 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
       return <Link id={props.id} target='_blank' rel={rel} href={href}>{children}</Link>
     },
     img: TextMediaOrLink,
-    embed: Embed
+    embed: Embed,
+    details: Details,
+    summary: Summary
   }), [outlawed, rel, TextMediaOrLink, topLevel])
 
   const carousel = useCarousel()
@@ -247,3 +249,19 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
     </div>
   )
 }
+
+function Summary ({ children }) {
+  return (
+    <summary className={styles.summary}>
+      {children}
+    </summary>
+  )
+}
+
+function Details ({ children }) {
+  return (
+    <details className={styles.details}>
+      {children}
+    </details>
+  )
+}
diff --git a/components/text.module.css b/components/text.module.css
index ff99b8a90..a8a3c7bcd 100644
--- a/components/text.module.css
+++ b/components/text.module.css
@@ -432,4 +432,4 @@
     max-width: 480px;
     border-radius: 13px;
     overflow: hidden;
-}
\ No newline at end of file
+}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index fb35bf4bd..73afc5445 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -2,6 +2,8 @@ import { SKIP, visit } from 'unist-util-visit'
 import { parseEmbedUrl, parseInternalLinks } from './url'
 import { slug } from 'github-slugger'
 import { toString } from 'mdast-util-to-string'
+import { fromMarkdown } from 'mdast-util-from-markdown'
+import { toHast } from 'mdast-util-to-hast'
 
 const userGroup = '[\\w_]+'
 const subGroup = '[A-Za-z][\\w_]+'
@@ -9,6 +11,8 @@ const subGroup = '[A-Za-z][\\w_]+'
 const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
+const detailsRegex = /<details>([\s\S]*?)<\/details>/
+const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
 
 export default function rehypeSN (options = {}) {
   const { stylers = [] } = options
@@ -20,13 +24,11 @@ export default function rehypeSN (options = {}) {
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
         }
-
         // handle headings
         if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
           const nodeText = toString(node)
           const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
           node.properties.id = headingId
-
           // Create a new link element
           const linkElement = {
             type: 'element',
@@ -36,7 +38,6 @@ export default function rehypeSN (options = {}) {
             },
             children: [{ type: 'text', value: nodeText }]
           }
-
           // Replace the heading's children with the new link element
           node.children = [linkElement]
           return [SKIP]
@@ -159,6 +160,7 @@ export default function rehypeSN (options = {}) {
 
         // handle custom tags
         if (node.type === 'element') {
+          // Existing stylers handling
           for (const { startTag, endTag, className } of stylers) {
             for (let i = 0; i < node.children.length - 2; i++) {
               const [start, text, end] = node.children.slice(i, i + 3)
@@ -214,6 +216,60 @@ export default function rehypeSN (options = {}) {
             return index + 1
           }
         }
+
+        // Handle details/summary tags
+        if (node.type === 'raw') {
+          const value = node.value.trim()
+
+          if (value.includes('<details>')) {
+          // Extract content between details tags
+            const detailsMatch = value.match(detailsRegex)
+            if (!detailsMatch) return
+
+            const fullContent = detailsMatch[1]
+
+            // Try to extract summary content if it exists
+            const summaryMatch = fullContent.match(summaryRegex)
+            const summaryContent = summaryMatch
+              ? summaryMatch[1].trim()
+              : 'Details' // Default summary text if none provided
+
+            // Get remaining content, accounting for optional summary
+            const remainingContent = summaryMatch
+              ? fullContent.replace(summaryMatch[0], '').trim()
+              : fullContent.trim()
+
+            // Convert markdown content to HTML AST nodes
+            // This allows proper rendering of markdown syntax like
+            // **bold**, *italic*, lists, etc. within details tags
+            const mdast = fromMarkdown(remainingContent)
+            const contentHast = toHast(mdast)
+
+            const summaryNode = {
+              type: 'element',
+              tagName: 'summary',
+              properties: {},
+              children: [{
+                type: 'text',
+                value: summaryContent
+              }]
+            }
+
+            const detailsNode = {
+              type: 'element',
+              tagName: 'details',
+              properties: {},
+              children: [
+                summaryNode,
+                ...(contentHast?.children ?? [])
+              ]
+            }
+
+            // Replace original node with new structure
+            parent.children[index] = detailsNode
+            return [SKIP]
+          }
+        }
       })
     } catch (error) {
       console.error('Error in rehypeSN transformer:', error)

From 5156da69e84e4255acc5b92b14b0898484b9e8c4 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sun, 17 Nov 2024 03:15:22 -0500
Subject: [PATCH 09/23] support details/summary tags, allow markdown inside

---
 lib/rehype-sn.js | 159 +++++++++++++++++++++++++++++++----------------
 1 file changed, 105 insertions(+), 54 deletions(-)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 73afc5445..fb88b0102 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -20,10 +20,73 @@ export default function rehypeSN (options = {}) {
   return function transformer (tree) {
     try {
       visit(tree, (node, index, parent) => {
+        // Process details/summary tags first to ensure proper node structure
+        if (node.type === 'raw' && parent?.children) {
+          // Check for incomplete details tags that might be split across nodes
+          if (node.value.includes('<details') && !node.value.includes('</details>')) {
+            let nextIndex = index + 1
+            let content = node.value
+
+            // Scan subsequent nodes until we find the closing details tag
+            while (nextIndex < parent.children.length) {
+              const nextNode = parent.children[nextIndex]
+              
+              if (nextNode.type === 'raw') {
+                content += nextNode.value
+                // Found closing tag - combine all nodes into one
+                if (nextNode.value.includes('</details>')) {
+                  node.value = content
+                  // Remove the now-combined nodes from parent
+                  parent.children.splice(index + 1, nextIndex - index)
+                  // Don't skip - we still need to process the combined content
+                  break
+                }
+              } else if (nextNode.type === 'text' || nextNode.type === 'element') {
+                // Preserve content from text nodes and elements (like paragraphs)
+                content += getTextContent(nextNode)
+              }
+              nextIndex++
+            }
+          }
+
+          // Process details tags (both complete and newly-combined ones)
+          const value = node.value.trim()
+          if (value.includes('<details>')) {
+            // Find all details blocks in this node
+            const detailsMatches = Array.from(value.matchAll(/<details>\s*([\s\S]*?)\s*<\/details>/g))
+            if (detailsMatches.length) {
+              detailsMatches.forEach(match => {
+                const content = match[1]
+                // Extract summary content if present
+                const summaryMatch = content.match(/<summary>([\s\S]*?)<\/summary>/)
+                const summaryContent = summaryMatch
+                  ? summaryMatch[1].trim()
+                  : 'Details' // Default summary text
+
+                // Get content after summary tag, or all content if no summary
+                let remainingContent = summaryMatch
+                  ? content.replace(summaryMatch[0], '').trim()
+                  : content.trim()
+
+                // Normalize markdown content
+                remainingContent = normalizeMarkdown(remainingContent)
+
+                // Convert markdown content to HTML structure
+                const mdast = fromMarkdown(remainingContent)
+                const contentHast = toHast(mdast)
+
+                Object.assign(node, createDetailsNode(summaryContent, contentHast?.children ?? []))
+              })
+              return [SKIP]
+            }
+          }
+        }
+
         // Handle inline code property
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
         }
+
         // handle headings
         if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
           const nodeText = toString(node)
@@ -216,60 +279,6 @@ export default function rehypeSN (options = {}) {
             return index + 1
           }
         }
-
-        // Handle details/summary tags
-        if (node.type === 'raw') {
-          const value = node.value.trim()
-
-          if (value.includes('<details>')) {
-          // Extract content between details tags
-            const detailsMatch = value.match(detailsRegex)
-            if (!detailsMatch) return
-
-            const fullContent = detailsMatch[1]
-
-            // Try to extract summary content if it exists
-            const summaryMatch = fullContent.match(summaryRegex)
-            const summaryContent = summaryMatch
-              ? summaryMatch[1].trim()
-              : 'Details' // Default summary text if none provided
-
-            // Get remaining content, accounting for optional summary
-            const remainingContent = summaryMatch
-              ? fullContent.replace(summaryMatch[0], '').trim()
-              : fullContent.trim()
-
-            // Convert markdown content to HTML AST nodes
-            // This allows proper rendering of markdown syntax like
-            // **bold**, *italic*, lists, etc. within details tags
-            const mdast = fromMarkdown(remainingContent)
-            const contentHast = toHast(mdast)
-
-            const summaryNode = {
-              type: 'element',
-              tagName: 'summary',
-              properties: {},
-              children: [{
-                type: 'text',
-                value: summaryContent
-              }]
-            }
-
-            const detailsNode = {
-              type: 'element',
-              tagName: 'details',
-              properties: {},
-              children: [
-                summaryNode,
-                ...(contentHast?.children ?? [])
-              ]
-            }
-
-            // Replace original node with new structure
-            parent.children[index] = detailsNode
-            return [SKIP]
-          }
-        }
       })
     } catch (error) {
       console.error('Error in rehypeSN transformer:', error)
@@ -330,4 +339,46 @@ export default function rehypeSN (options = {}) {
       children: [{ type: 'text', value }]
     }
   }
+
+  /**
+   * Extracts text content from any node type
+   * Handles both direct text nodes and nested element structures
+   */
+  function getTextContent (node) {
+    if (node.value) return node.value
+    if (!node.children) return ''
+    return node.children.map(child => getTextContent(child)).join('')
+  }
+
+  // Helper function to normalize markdown content
+  function normalizeMarkdown(content) {
+    return content
+      .replace(/\r\n/g, '\n')           // Normalize line endings
+      .split('\n')                      // Split into lines
+      .map(line => line.trim())         // Trim each line
+      .join('\n\n')                     // Add blank line between all content
+      .replace(/\n{3,}/g, '\n\n')       // Normalize multiple blank lines to two
+      .trim()                           // Trim final result
+  }
+
+  // Helper function to create details node structure
+  function createDetailsNode(summaryContent, children) {
+    return {
+      type: 'element',
+      tagName: 'details',
+      properties: {},
+      children: [
+        {
+          type: 'element',
+          tagName: 'summary',
+          properties: {},
+          children: [{
+            type: 'text',
+            value: summaryContent
+          }]
+        },
+        ...children
+      ]
+    }
+  }
 }

From a5a90b8923d87303a6315ad1726f3317f1d245eb Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sun, 17 Nov 2024 03:22:09 -0500
Subject: [PATCH 10/23] fix: linting

---
 lib/rehype-sn.js | 32 +++++++++-----------------------
 1 file changed, 9 insertions(+), 23 deletions(-)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index fb88b0102..873674c49 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -11,8 +11,6 @@ const subGroup = '[A-Za-z][\\w_]+'
 const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
-const detailsRegex = /<details>([\s\S]*?)<\/details>/
-const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
 
 export default function rehypeSN (options = {}) {
   const { stylers = [] } = options
@@ -20,9 +18,7 @@ export default function rehypeSN (options = {}) {
   return function transformer (tree) {
     try {
       visit(tree, (node, index, parent) => {
-        // Process details/summary tags first to ensure proper node structure
         if (node.type === 'raw' && parent?.children) {
-          // Check for incomplete details tags that might be split across nodes
           if (node.value.includes('<details') && !node.value.includes('</details>')) {
             let nextIndex = index + 1
             let content = node.value
@@ -30,15 +26,11 @@ export default function rehypeSN (options = {}) {
             // Scan subsequent nodes until we find the closing details tag
             while (nextIndex < parent.children.length) {
               const nextNode = parent.children[nextIndex]
-              
               if (nextNode.type === 'raw') {
                 content += nextNode.value
-                // Found closing tag - combine all nodes into one
                 if (nextNode.value.includes('</details>')) {
                   node.value = content
-                  // Remove the now-combined nodes from parent
                   parent.children.splice(index + 1, nextIndex - index)
-                  // Don't skip - we still need to process the combined content
                   break
                 }
               } else if (nextNode.type === 'text' || nextNode.type === 'element') {
@@ -52,16 +44,13 @@ export default function rehypeSN (options = {}) {
           // Process details tags (both complete and newly-combined ones)
           const value = node.value.trim()
           if (value.includes('<details>')) {
-            // Find all details blocks in this node
             const detailsMatches = Array.from(value.matchAll(/<details>\s*([\s\S]*?)\s*<\/details>/g))
             if (detailsMatches.length) {
               detailsMatches.forEach(match => {
                 const content = match[1]
                 // Extract summary content if present
                 const summaryMatch = content.match(/<summary>([\s\S]*?)<\/summary>/)
-                const summaryContent = summaryMatch
-                  ? summaryMatch[1].trim()
-                  : 'Details' // Default summary text
+                const summaryContent = summaryMatch ? summaryMatch[1].trim() : 'Details'
 
                 // Get content after summary tag, or all content if no summary
                 let remainingContent = summaryMatch
@@ -223,7 +212,6 @@ export default function rehypeSN (options = {}) {
 
         // handle custom tags
         if (node.type === 'element') {
-          // Existing stylers handling
           for (const { startTag, endTag, className } of stylers) {
             for (let i = 0; i < node.children.length - 2; i++) {
               const [start, text, end] = node.children.slice(i, i + 3)
@@ -350,19 +338,17 @@ export default function rehypeSN (options = {}) {
     return node.children.map(child => getTextContent(child)).join('')
   }
 
-  // Helper function to normalize markdown content
-  function normalizeMarkdown(content) {
+  function normalizeMarkdown (content) {
     return content
-      .replace(/\r\n/g, '\n')           // Normalize line endings
-      .split('\n')                      // Split into lines
-      .map(line => line.trim())         // Trim each line
-      .join('\n\n')                     // Add blank line between all content
-      .replace(/\n{3,}/g, '\n\n')       // Normalize multiple blank lines to two
-      .trim()                           // Trim final result
+      .replace(/\r\n/g, '\n')
+      .split('\n')
+      .map(line => line.trim())
+      .join('\n\n')
+      .replace(/\n{3,}/g, '\n\n')
+      .trim()
   }
 
-  // Helper function to create details node structure
-  function createDetailsNode(summaryContent, children) {
+  function createDetailsNode (summaryContent, children) {
     return {
       type: 'element',
       tagName: 'details',

From 1015cb2b3c5e5c55d972d028600d0591d30a8c53 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 18 Nov 2024 01:14:39 -0500
Subject: [PATCH 11/23] Working structure 2, with or without blank lines

---
 components/text.js |  21 +-----
 lib/rehype-sn.js   | 171 +++++++++++++++++++++------------------------
 2 files changed, 82 insertions(+), 110 deletions(-)

diff --git a/components/text.js b/components/text.js
index 52b8b959b..7f5f214a2 100644
--- a/components/text.js
+++ b/components/text.js
@@ -125,8 +125,8 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
     },
     img: TextMediaOrLink,
     embed: Embed,
-    details: Details,
-    summary: Summary
+    details: ({ node, children, ...props }) => <details {...props}>{children}</details>,
+    summary: ({ node, children, ...props }) => <summary {...props}>{children}</summary>
   }), [outlawed, rel, TextMediaOrLink, topLevel])
 
   const carousel = useCarousel()
@@ -225,6 +225,7 @@ function Table ({ node, ...props }) {
   )
 }
 
+
 function Code ({ node, inline, className, children, style, ...props }) {
   return inline
     ? (
@@ -249,19 +250,3 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
     </div>
   )
 }
-
-function Summary ({ children }) {
-  return (
-    <summary className={styles.summary}>
-      {children}
-    </summary>
-  )
-}
-
-function Details ({ children }) {
-  return (
-    <details className={styles.details}>
-      {children}
-    </details>
-  )
-}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 873674c49..fe70d5bfe 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -3,6 +3,8 @@ import { parseEmbedUrl, parseInternalLinks } from './url'
 import { slug } from 'github-slugger'
 import { toString } from 'mdast-util-to-string'
 import { fromMarkdown } from 'mdast-util-from-markdown'
+import { gfm } from 'micromark-extension-gfm'
+import { gfmFromMarkdown } from 'mdast-util-gfm'
 import { toHast } from 'mdast-util-to-hast'
 
 const userGroup = '[\\w_]+'
@@ -18,59 +20,18 @@ export default function rehypeSN (options = {}) {
   return function transformer (tree) {
     try {
       visit(tree, (node, index, parent) => {
-        if (node.type === 'raw' && parent?.children) {
-          if (node.value.includes('<details') && !node.value.includes('</details>')) {
-            let nextIndex = index + 1
-            let content = node.value
-
-            // Scan subsequent nodes until we find the closing details tag
-            while (nextIndex < parent.children.length) {
-              const nextNode = parent.children[nextIndex]
-              if (nextNode.type === 'raw') {
-                content += nextNode.value
-                if (nextNode.value.includes('</details>')) {
-                  node.value = content
-                  parent.children.splice(index + 1, nextIndex - index)
-                  break
-                }
-              } else if (nextNode.type === 'text' || nextNode.type === 'element') {
-                // Preserve content from text nodes and elements (like paragraphs)
-                content += getTextContent(nextNode)
-              }
-              nextIndex++
-            }
-          }
-
-          // Process details tags (both complete and newly-combined ones)
-          const value = node.value.trim()
-          if (value.includes('<details>')) {
-            const detailsMatches = Array.from(value.matchAll(/<details>\s*([\s\S]*?)\s*<\/details>/g))
-            if (detailsMatches.length) {
-              detailsMatches.forEach(match => {
-                const content = match[1]
-                // Extract summary content if present
-                const summaryMatch = content.match(/<summary>([\s\S]*?)<\/summary>/)
-                const summaryContent = summaryMatch ? summaryMatch[1].trim() : 'Details'
-
-                // Get content after summary tag, or all content if no summary
-                let remainingContent = summaryMatch
-                  ? content.replace(summaryMatch[0], '').trim()
-                  : content.trim()
-
-                // Normalize markdown content
-                remainingContent = normalizeMarkdown(remainingContent)
-
-                // Convert markdown content to HTML structure
-                const mdast = fromMarkdown(remainingContent)
-                const contentHast = toHast(mdast)
-
-                Object.assign(node, createDetailsNode(summaryContent, contentHast?.children ?? []))
-              })
-              return [SKIP]
-            }
-          }
+        // Log node info for debugging
+        const nodeInfo = {
+          ...(node.type && { type: node.type }),
+          ...(node.tagName && { tagName: node.tagName }),
+          ...(node.properties && { properties: node.properties }),
+          ...(node.value && { value: node.value }),
+          ...(node.children && { children: `${node.children.length} children` }),
+          ...(parent?.type && { parentType: parent.type }),
+          ...(parent?.tagName && { parentTagName: parent.tagName }),
+          index
         }
-
+        console.log('Node:', nodeInfo)
         // Handle inline code property
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
@@ -267,6 +228,72 @@ export default function rehypeSN (options = {}) {
             return index + 1
           }
         }
+        // Handle details tags
+        if (node.type === 'raw' && node.value?.includes('<details>')) {
+          // Find the start of our details block
+          const detailsMatch = node.value.match(/<details>\s*<summary>(.*?)<\/summary>(.*?)$/s)
+          if (detailsMatch) {
+            const [_, summary, initialContent] = detailsMatch
+            
+            // Collect all content until we find the closing details tag
+            let content = initialContent || ''  // Include any content after summary
+            let currentIndex = index + 1
+            const contentNodes = []
+            
+            while (currentIndex < parent.children.length) {
+              const currentNode = parent.children[currentIndex]
+              if (currentNode.type === 'raw' && currentNode.value === '</details>') {
+                break
+              }
+              contentNodes.push(currentNode)
+              currentIndex++
+            }
+            
+            // Convert content nodes to text, preserving markdown syntax
+            content += contentNodes
+              .map(node => {
+                if (node.type === 'text') return node.value
+                if (node.type === 'element' && node.tagName === 'ul') {
+                  return node.children
+                    .filter(child => child.tagName === 'li')
+                    .map(li => `- ${li.children[0].value}`)
+                    .join('\n')
+                }
+                return ''
+              })
+              .join('')
+
+            // Parse content to markdown AST
+            const mdast = fromMarkdown(content, {
+              extensions: [gfm()],
+              mdastExtensions: [gfmFromMarkdown()]
+            })
+
+            // Convert markdown AST to HTML AST
+            const contentHast = toHast(mdast)
+
+            const detailsNode = {
+              type: 'element',
+              tagName: 'details',
+              properties: {},
+              children: [
+                {
+                  type: 'element',
+                  tagName: 'summary',
+                  properties: {},
+                  children: [
+                    { type: 'text', value: summary }
+                  ]
+                },
+                ...contentHast.children
+              ]
+            }
+            
+            // Replace all nodes from details start to end with our new node
+            parent.children.splice(index, currentIndex - index + 1, detailsNode)
+            return index + 1
+          }
+        }
       })
     } catch (error) {
       console.error('Error in rehypeSN transformer:', error)
@@ -327,44 +354,4 @@ export default function rehypeSN (options = {}) {
       children: [{ type: 'text', value }]
     }
   }
-
-  /**
-   * Extracts text content from any node type
-   * Handles both direct text nodes and nested element structures
-   */
-  function getTextContent (node) {
-    if (node.value) return node.value
-    if (!node.children) return ''
-    return node.children.map(child => getTextContent(child)).join('')
-  }
-
-  function normalizeMarkdown (content) {
-    return content
-      .replace(/\r\n/g, '\n')
-      .split('\n')
-      .map(line => line.trim())
-      .join('\n\n')
-      .replace(/\n{3,}/g, '\n\n')
-      .trim()
-  }
-
-  function createDetailsNode (summaryContent, children) {
-    return {
-      type: 'element',
-      tagName: 'details',
-      properties: {},
-      children: [
-        {
-          type: 'element',
-          tagName: 'summary',
-          properties: {},
-          children: [{
-            type: 'text',
-            value: summaryContent
-          }]
-        },
-        ...children
-      ]
-    }
-  }
 }

From b8b02ced9ae8faf73f8958e0142137c75fe1ffec Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Sun, 24 Nov 2024 00:37:06 -0500
Subject: [PATCH 12/23] Makeover checkpoint: close to working with newlines
 breaking tags or markdown

---
 components/text.js |  30 ++++-
 lib/rehype-sn.js   | 302 +++++++++++++++++++++++++++++++++++----------
 2 files changed, 264 insertions(+), 68 deletions(-)

diff --git a/components/text.js b/components/text.js
index 7f5f214a2..748111e1f 100644
--- a/components/text.js
+++ b/components/text.js
@@ -125,8 +125,8 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
     },
     img: TextMediaOrLink,
     embed: Embed,
-    details: ({ node, children, ...props }) => <details {...props}>{children}</details>,
-    summary: ({ node, children, ...props }) => <summary {...props}>{children}</summary>
+    details: Details,
+    summary: Summary,
   }), [outlawed, rel, TextMediaOrLink, topLevel])
 
   const carousel = useCarousel()
@@ -250,3 +250,29 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
     </div>
   )
 }
+
+function Details({ children, node, ...props }) {
+  const [isOpen, setIsOpen] = useState(false)
+
+  const handleToggle = (e) => {
+    setIsOpen(e.target.open)
+  }
+
+  return (
+    <details 
+      className={styles.details} 
+      {...props} 
+      onToggle={handleToggle}
+    >
+      {children}
+    </details>
+  )
+}
+
+function Summary({ children, node, ...props }) {
+  return (
+    <summary className={styles.summary} {...props}>
+      {children}
+    </summary>
+  )
+}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index fe70d5bfe..0be0737c9 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -32,6 +32,134 @@ export default function rehypeSN (options = {}) {
           index
         }
         console.log('Node:', nodeInfo)
+        // Handle details tags
+        if (node.type === 'raw' && node.value.includes('<details>')) {
+          console.log('Details tag found')
+          
+          // Find all content between opening and closing details tags
+          let detailsContent = []
+          let summaryText = ''
+          let i = index
+          let foundClosingTag = false
+          let inSummary = false
+          
+          // First check if opening and closing tags are in the same node
+          if (node.value.includes('</details>')) {
+            // Extract content between tags from single node
+            const content = node.value.slice(
+              node.value.indexOf('<details>') + 9,
+              node.value.indexOf('</details>')
+            )
+            
+            // Check for summary tag
+            if (content.includes('<summary>')) {
+              const summaryStart = content.indexOf('<summary>') + 9
+              const summaryEnd = content.indexOf('</summary>')
+              summaryText = content.slice(summaryStart, summaryEnd).trim()
+              
+              // Get content after summary
+              const afterSummary = content.slice(summaryEnd + 10).trim()
+              if (afterSummary) {
+                detailsContent.push({
+                  type: 'text',
+                  value: afterSummary
+                })
+              }
+            } else {
+              // No summary tag, use content as-is
+              detailsContent.push({
+                type: 'text', 
+                value: content.trim()
+              })
+            }
+            
+            foundClosingTag = true
+            i = index
+            
+          } else {
+            // Need to traverse nodes to find closing tag
+            while (i < parent.children.length) {
+              const currentNode = parent.children[i]
+              
+              // Check if we've hit the closing tag
+              if (currentNode.type === 'raw' && currentNode.value.includes('</details>')) {
+                // Get any content before closing tag
+                const beforeClosing = currentNode.value.slice(0, currentNode.value.indexOf('</details>')).trim()
+                if (beforeClosing) {
+                  detailsContent.push({
+                    type: 'text',
+                    value: beforeClosing
+                  })
+                }
+                foundClosingTag = true
+                break
+              }
+
+              // Handle summary tags
+              if (currentNode.type === 'raw' && currentNode.value.includes('<summary>')) {
+                inSummary = true
+                // Get content after summary opening tag
+                const afterOpening = currentNode.value.slice(currentNode.value.indexOf('<summary>') + 9).trim()
+                if (afterOpening) {
+                  summaryText += afterOpening
+                }
+              } else if (currentNode.type === 'raw' && currentNode.value.includes('</summary>')) {
+                inSummary = false
+                // Get content before summary closing tag
+                const beforeClosing = currentNode.value.slice(0, currentNode.value.indexOf('</summary>')).trim()
+                if (beforeClosing) {
+                  summaryText += beforeClosing
+                }
+              } else if (inSummary) {
+                // Collect summary text
+                if (currentNode.type === 'text') {
+                  summaryText += currentNode.value
+                } else if (currentNode.type === 'element') {
+                  summaryText += toString(currentNode)
+                }
+              } else if (!inSummary && i > index) {
+                // Skip the opening details node content
+                if (currentNode.type === 'text' || currentNode.type === 'element') {
+                  detailsContent.push(currentNode)
+                } else if (currentNode.type === 'raw' && !currentNode.value.includes('<details>')) {
+                  detailsContent.push({
+                    type: 'text',
+                    value: currentNode.value
+                  })
+                }
+              }
+
+              i++
+            }
+          }
+
+          // Only process if we found a proper closing tag
+          if (foundClosingTag) {
+            // Convert collected content nodes to markdown string
+            const markdownContent = detailsContent
+              .map(node => toString(node))
+              .join('\n')
+              .trim()
+
+            // Use helper to create new details structure
+            const newDetailsNode = alternateCreateDetails(
+              markdownContent,
+              summaryText.trim() || 'Details'
+            )
+
+            console.log('Details content:', {
+              markdownContent,
+              summaryText: summaryText.trim(),
+              detailsContent,
+              newDetailsNode
+            })
+
+            // Replace original nodes with new details structure
+            parent.children.splice(index, i - index + 1, newDetailsNode)
+
+            return index
+          }
+        }
         // Handle inline code property
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
@@ -228,72 +356,6 @@ export default function rehypeSN (options = {}) {
             return index + 1
           }
         }
-        // Handle details tags
-        if (node.type === 'raw' && node.value?.includes('<details>')) {
-          // Find the start of our details block
-          const detailsMatch = node.value.match(/<details>\s*<summary>(.*?)<\/summary>(.*?)$/s)
-          if (detailsMatch) {
-            const [_, summary, initialContent] = detailsMatch
-            
-            // Collect all content until we find the closing details tag
-            let content = initialContent || ''  // Include any content after summary
-            let currentIndex = index + 1
-            const contentNodes = []
-            
-            while (currentIndex < parent.children.length) {
-              const currentNode = parent.children[currentIndex]
-              if (currentNode.type === 'raw' && currentNode.value === '</details>') {
-                break
-              }
-              contentNodes.push(currentNode)
-              currentIndex++
-            }
-            
-            // Convert content nodes to text, preserving markdown syntax
-            content += contentNodes
-              .map(node => {
-                if (node.type === 'text') return node.value
-                if (node.type === 'element' && node.tagName === 'ul') {
-                  return node.children
-                    .filter(child => child.tagName === 'li')
-                    .map(li => `- ${li.children[0].value}`)
-                    .join('\n')
-                }
-                return ''
-              })
-              .join('')
-
-            // Parse content to markdown AST
-            const mdast = fromMarkdown(content, {
-              extensions: [gfm()],
-              mdastExtensions: [gfmFromMarkdown()]
-            })
-
-            // Convert markdown AST to HTML AST
-            const contentHast = toHast(mdast)
-
-            const detailsNode = {
-              type: 'element',
-              tagName: 'details',
-              properties: {},
-              children: [
-                {
-                  type: 'element',
-                  tagName: 'summary',
-                  properties: {},
-                  children: [
-                    { type: 'text', value: summary }
-                  ]
-                },
-                ...contentHast.children
-              ]
-            }
-            
-            // Replace all nodes from details start to end with our new node
-            parent.children.splice(index, currentIndex - index + 1, detailsNode)
-            return index + 1
-          }
-        }
       })
     } catch (error) {
       console.error('Error in rehypeSN transformer:', error)
@@ -354,4 +416,112 @@ export default function rehypeSN (options = {}) {
       children: [{ type: 'text', value }]
     }
   }
+  function createDetails (markdownContent, summaryText) {
+    return {
+      type: 'element',
+      tagName: 'details',
+      properties: {},
+      children: [
+        {
+          type: 'element',
+          tagName: 'summary',
+          properties: {},
+          children: [{ type: 'text', value: summaryText }]
+        },
+        {
+          type: 'element',
+          tagName: 'div',
+          properties: {},
+          children: markdownContent.split('\n').map(line => ({
+            type: 'element',
+            tagName: 'p',
+            properties: {},
+            children: [{ type: 'text', value: line }]
+          }))
+        }
+      ]
+    }
+  }
+  function alternateCreateDetails(markdownContent, summaryText) {
+    // Parse markdown content into mdast (markdown abstract syntax tree)
+    const mdastContent = fromMarkdown(markdownContent, {
+      extensions: [gfm()],
+      mdastExtensions: [gfmFromMarkdown()]
+    })
+  
+    // Convert mdast to hast (HTML abstract syntax tree)
+    const hastContent = toHast(mdastContent)
+  
+    return {
+      type: 'element',
+      tagName: 'details',
+      properties: {},
+      children: [
+        {
+          type: 'element',
+          tagName: 'summary',
+          properties: {},
+          children: [{ type: 'text', value: summaryText }]
+        },
+        {
+          type: 'element',
+          tagName: 'div',
+          properties: {},
+          children: hastContent.children
+        }
+      ]
+    }
+  }
 }
+
+
+// Different structures that must be handled properly and not break details/summary
+
+// Structure 1:
+
+// <details>no summary tags
+// lorem ipsum
+// and text on a new line</details>
+
+// ----------
+
+// Structure 2:
+
+// <details>
+// <summary>text inside summary</summary>
+
+// lorem ipsum
+// </details>
+
+// ----------
+
+// Structure 3:
+
+// <details>
+//     <summary>summary indentation single line</summary>
+// 1. first thing
+// 2. second thing 
+// 3. third thing
+// </details>
+
+// ----------
+
+// Structure 4:
+
+// <details>
+//   <summary>
+//     summary text here
+//   </summary>
+//   text inside details with *markdown* working **properly**
+// </details>
+
+// ----------
+
+// Structure 5:
+
+// <details>
+// <summary>Shopping list</summary>
+// - Vegetables
+// - Fruits
+// - Fish
+// </details>

From cde3f12e14ee4b67149e2c06e5f30a8e2a047282 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 2 Dec 2024 01:07:53 -0500
Subject: [PATCH 13/23] Handle blanklines (extra newlines) that break
 formatting

---
 lib/rehype-sn.js | 346 +++++++++++++++++++++++++----------------------
 1 file changed, 184 insertions(+), 162 deletions(-)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 0be0737c9..4f6e57e26 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -14,11 +14,68 @@ const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)',
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
 
+function alternateCreateDetails(markdownContent, summaryText) {
+  // Parse both summary and content as markdown
+  const mdastSummary = fromMarkdown(summaryText, {
+    extensions: [gfm()],
+    mdastExtensions: [gfmFromMarkdown()]
+  })
+
+  const mdastContent = fromMarkdown(markdownContent, {
+    extensions: [gfm()],
+    mdastExtensions: [gfmFromMarkdown()]
+  })
+
+  // Convert both to hast
+  const hastSummary = toHast(mdastSummary)
+  const hastContent = toHast(mdastContent)
+
+  // For summary, we want to ensure the content stays inline
+  // If hastSummary has block elements, we need to extract their text content
+  const flattenBlockElements = (node) => {
+    if (node.type === 'text') return [node]
+    if (!node.children) return []
+    
+    return node.children.flatMap(child => {
+      if (child.type === 'text') return [child]
+      if (child.type === 'element') {
+        // Convert block elements to spans to keep them inline
+        if (['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) {
+          return child.children.flatMap(flattenBlockElements)
+        }
+        return [child]
+      }
+      return flattenBlockElements(child)
+    })
+  }
+
+  const summaryChildren = hastSummary.children.flatMap(flattenBlockElements)
+
+  // Create the details structure
+  return {
+    type: 'element',
+    tagName: 'details',
+    properties: {},
+    children: [
+      {
+        type: 'element',
+        tagName: 'summary',
+        properties: {},
+        children: summaryChildren
+      },
+      // For the content, we want to preserve block formatting
+      ...hastContent.children
+    ]
+  }
+}
+
 export default function rehypeSN (options = {}) {
   const { stylers = [] } = options
 
   return function transformer (tree) {
     try {
+      let detailsStack = []  // Track nested details processing
+
       visit(tree, (node, index, parent) => {
         // Log node info for debugging
         const nodeInfo = {
@@ -32,134 +89,154 @@ export default function rehypeSN (options = {}) {
           index
         }
         console.log('Node:', nodeInfo)
+
         // Handle details tags
-        if (node.type === 'raw' && node.value.includes('<details>')) {
-          console.log('Details tag found')
+        if (node.type === 'raw' && (
+          node.value.includes('<details>') || 
+          node.value.includes('<details >')
+        )) {
+          console.log('Details opening tag found')
           
-          // Find all content between opening and closing details tags
-          let detailsContent = []
-          let summaryText = ''
-          let i = index
-          let foundClosingTag = false
-          let inSummary = false
-          
-          // First check if opening and closing tags are in the same node
+          // Check if this is a single-line details tag
           if (node.value.includes('</details>')) {
-            // Extract content between tags from single node
-            const content = node.value.slice(
-              node.value.indexOf('<details>') + 9,
-              node.value.indexOf('</details>')
-            )
+            // Extract content between tags
+            const fullContent = node.value
+            let content = fullContent
+              .replace(/<details>|<details >/, '')
+              .replace('</details>', '')
             
-            // Check for summary tag
-            if (content.includes('<summary>')) {
+            let summaryText = 'Details'
+            
+            // Check for summary tags
+            if (content.includes('<summary>') && content.includes('</summary>')) {
               const summaryStart = content.indexOf('<summary>') + 9
               const summaryEnd = content.indexOf('</summary>')
-              summaryText = content.slice(summaryStart, summaryEnd).trim()
+              summaryText = content.slice(summaryStart, summaryEnd)
               
-              // Get content after summary
+              // Get content before and after summary
+              const beforeSummary = content.slice(0, content.indexOf('<summary>')).trim()
               const afterSummary = content.slice(summaryEnd + 10).trim()
-              if (afterSummary) {
-                detailsContent.push({
-                  type: 'text',
-                  value: afterSummary
-                })
-              }
-            } else {
-              // No summary tag, use content as-is
-              detailsContent.push({
-                type: 'text', 
-                value: content.trim()
-              })
+              
+              content = [beforeSummary, afterSummary].filter(Boolean).join('\n')
             }
             
-            foundClosingTag = true
-            i = index
+            // Create the details node directly
+            const newDetailsNode = alternateCreateDetails(
+              content.trim(),
+              summaryText
+            )
             
-          } else {
-            // Need to traverse nodes to find closing tag
-            while (i < parent.children.length) {
-              const currentNode = parent.children[i]
-              
-              // Check if we've hit the closing tag
-              if (currentNode.type === 'raw' && currentNode.value.includes('</details>')) {
-                // Get any content before closing tag
-                const beforeClosing = currentNode.value.slice(0, currentNode.value.indexOf('</details>')).trim()
-                if (beforeClosing) {
-                  detailsContent.push({
-                    type: 'text',
-                    value: beforeClosing
-                  })
-                }
-                foundClosingTag = true
-                break
-              }
+            // Replace the current node
+            parent.children[index] = newDetailsNode
+            return index
+          }
+          
+          // Initialize details processing state for multi-line case
+          detailsStack.push({
+            startIndex: index,
+            contentBuffer: [],
+            summaryBuffer: [],
+            inSummary: false,
+            foundSummary: false,
+            summaryText: '',
+            detailsContent: [],
+            processedNodes: new Set() // Track which nodes we've processed
+          })
+
+          return
+        }
 
-              // Handle summary tags
-              if (currentNode.type === 'raw' && currentNode.value.includes('<summary>')) {
-                inSummary = true
-                // Get content after summary opening tag
-                const afterOpening = currentNode.value.slice(currentNode.value.indexOf('<summary>') + 9).trim()
-                if (afterOpening) {
-                  summaryText += afterOpening
-                }
-              } else if (currentNode.type === 'raw' && currentNode.value.includes('</summary>')) {
-                inSummary = false
-                // Get content before summary closing tag
-                const beforeClosing = currentNode.value.slice(0, currentNode.value.indexOf('</summary>')).trim()
-                if (beforeClosing) {
-                  summaryText += beforeClosing
-                }
-              } else if (inSummary) {
-                // Collect summary text
-                if (currentNode.type === 'text') {
-                  summaryText += currentNode.value
-                } else if (currentNode.type === 'element') {
-                  summaryText += toString(currentNode)
-                }
-              } else if (!inSummary && i > index) {
-                // Skip the opening details node content
-                if (currentNode.type === 'text' || currentNode.type === 'element') {
-                  detailsContent.push(currentNode)
-                } else if (currentNode.type === 'raw' && !currentNode.value.includes('<details>')) {
-                  detailsContent.push({
-                    type: 'text',
-                    value: currentNode.value
-                  })
-                }
-              }
+        // Process details content if we're inside a details tag
+        if (detailsStack.length > 0) {
+          const state = detailsStack[detailsStack.length - 1]
+          
+          // Skip if we've already processed this node
+          if (state.processedNodes.has(index)) {
+            return
+          }
+          state.processedNodes.add(index)
 
-              i++
+          // Check for closing details tag
+          if (node.type === 'raw' && node.value.includes('</details>')) {
+            console.log('Details closing tag found')
+            detailsStack.pop()
+            
+            // Get any content before the closing tag
+            const beforeClosing = node.value.split('</details>')[0]
+            if (beforeClosing) {
+              state.contentBuffer.push(beforeClosing)
             }
-          }
 
-          // Only process if we found a proper closing tag
-          if (foundClosingTag) {
-            // Convert collected content nodes to markdown string
-            const markdownContent = detailsContent
-              .map(node => toString(node))
+            // Process all collected content
+            const content = state.contentBuffer
+              .filter(Boolean) // Remove empty strings
               .join('\n')
+              .replace(/\n{3,}/g, '\n\n') // Replace 3 or more newlines with 2
               .trim()
 
-            // Use helper to create new details structure
+            // Create the details node
+            const markdownContent = state.detailsContent
+              .map(node => toString(node))
+              .join('\n')
+
             const newDetailsNode = alternateCreateDetails(
-              markdownContent,
-              summaryText.trim() || 'Details'
+              content || markdownContent,
+              state.summaryText ? state.summaryText.trim() : 'Details'
             )
 
-            console.log('Details content:', {
-              markdownContent,
-              summaryText: summaryText.trim(),
-              detailsContent,
-              newDetailsNode
-            })
-
-            // Replace original nodes with new details structure
-            parent.children.splice(index, i - index + 1, newDetailsNode)
+            // Replace all nodes from start to current with new details node
+            parent.children.splice(state.startIndex, index - state.startIndex + 1, newDetailsNode)
+            
+            return state.startIndex
+          }
 
-            return index
+          // Handle summary tags
+          if (node.type === 'raw' && node.value.includes('<summary>')) {
+            state.inSummary = true
+            state.foundSummary = true
+            const parts = node.value.split('<summary>')
+            if (parts[0]) {
+              state.contentBuffer.push(parts[0])
+            }
+            if (parts[1]) {
+              state.summaryBuffer.push(parts[1])
+            }
+          } else if (node.type === 'raw' && node.value.includes('</summary>')) {
+            state.inSummary = false
+            const parts = node.value.split('</summary>')
+            if (parts[0]) {
+              state.summaryBuffer.push(parts[0])
+            }
+            if (parts[1]) {
+              state.contentBuffer.push(parts[1])
+            }
+            state.summaryText = state.summaryBuffer.join('\n')
+          } else if (state.inSummary) {
+            // Collect summary text
+            if (node.type === 'text' || node.type === 'raw') {
+              state.summaryBuffer.push(node.value)
+            } else if (node.type === 'element') {
+              state.summaryBuffer.push(toString(node))
+            }
+          } else {
+            // Handle content collection
+            if (node.type === 'text') {
+              // Only add non-empty text nodes or preserve single newlines
+              if (node.value.trim() || node.value === '\n') {
+                state.contentBuffer.push(node.value)
+              }
+            } else if (node.type === 'element') {
+              state.contentBuffer.push(toString(node))
+            } else if (node.type === 'raw' && 
+                       !node.value.includes('<details>') &&
+                       !node.value.includes('</details>')) {
+              state.contentBuffer.push(node.value)
+            }
           }
+
+          return
         }
+
         // Handle inline code property
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
@@ -357,11 +434,12 @@ export default function rehypeSN (options = {}) {
           }
         }
       })
+
+      return tree
     } catch (error) {
       console.error('Error in rehypeSN transformer:', error)
+      return tree
     }
-
-    return tree
   }
 
   function isImageOnlyParagraph (node) {
@@ -416,62 +494,6 @@ export default function rehypeSN (options = {}) {
       children: [{ type: 'text', value }]
     }
   }
-  function createDetails (markdownContent, summaryText) {
-    return {
-      type: 'element',
-      tagName: 'details',
-      properties: {},
-      children: [
-        {
-          type: 'element',
-          tagName: 'summary',
-          properties: {},
-          children: [{ type: 'text', value: summaryText }]
-        },
-        {
-          type: 'element',
-          tagName: 'div',
-          properties: {},
-          children: markdownContent.split('\n').map(line => ({
-            type: 'element',
-            tagName: 'p',
-            properties: {},
-            children: [{ type: 'text', value: line }]
-          }))
-        }
-      ]
-    }
-  }
-  function alternateCreateDetails(markdownContent, summaryText) {
-    // Parse markdown content into mdast (markdown abstract syntax tree)
-    const mdastContent = fromMarkdown(markdownContent, {
-      extensions: [gfm()],
-      mdastExtensions: [gfmFromMarkdown()]
-    })
-  
-    // Convert mdast to hast (HTML abstract syntax tree)
-    const hastContent = toHast(mdastContent)
-  
-    return {
-      type: 'element',
-      tagName: 'details',
-      properties: {},
-      children: [
-        {
-          type: 'element',
-          tagName: 'summary',
-          properties: {},
-          children: [{ type: 'text', value: summaryText }]
-        },
-        {
-          type: 'element',
-          tagName: 'div',
-          properties: {},
-          children: hastContent.children
-        }
-      ]
-    }
-  }
 }
 
 

From 684e5aece020bff486984ae6cd066932551df1ea Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 2 Dec 2024 01:57:04 -0500
Subject: [PATCH 14/23] Refactoring and reorganize, cleanup

---
 lib/rehype-sn-OLD.js | 277 +++++++++++++++++++++++++++++++++++++++++++
 lib/rehype-sn.js     | 204 ++++++++++++++-----------------
 2 files changed, 365 insertions(+), 116 deletions(-)
 create mode 100644 lib/rehype-sn-OLD.js

diff --git a/lib/rehype-sn-OLD.js b/lib/rehype-sn-OLD.js
new file mode 100644
index 000000000..fb35bf4bd
--- /dev/null
+++ b/lib/rehype-sn-OLD.js
@@ -0,0 +1,277 @@
+import { SKIP, visit } from 'unist-util-visit'
+import { parseEmbedUrl, parseInternalLinks } from './url'
+import { slug } from 'github-slugger'
+import { toString } from 'mdast-util-to-string'
+
+const userGroup = '[\\w_]+'
+const subGroup = '[A-Za-z][\\w_]+'
+
+const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
+const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
+const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
+
+export default function rehypeSN (options = {}) {
+  const { stylers = [] } = options
+
+  return function transformer (tree) {
+    try {
+      visit(tree, (node, index, parent) => {
+        // Handle inline code property
+        if (node.tagName === 'code') {
+          node.properties.inline = !(parent && parent.tagName === 'pre')
+        }
+
+        // handle headings
+        if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
+          const nodeText = toString(node)
+          const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
+          node.properties.id = headingId
+
+          // Create a new link element
+          const linkElement = {
+            type: 'element',
+            tagName: 'headlink',
+            properties: {
+              href: `#${headingId}`
+            },
+            children: [{ type: 'text', value: nodeText }]
+          }
+
+          // Replace the heading's children with the new link element
+          node.children = [linkElement]
+          return [SKIP]
+        }
+
+        // if img is wrapped in a link, remove the link
+        if (node.tagName === 'a' && node.children.length === 1 && node.children[0].tagName === 'img') {
+          parent.children[index] = node.children[0]
+          return index
+        }
+
+        // handle internal links
+        if (node.tagName === 'a') {
+          try {
+            if (node.properties.href.includes('#itemfn-')) {
+              node.tagName = 'footnote'
+            } else {
+              const { itemId, linkText } = parseInternalLinks(node.properties.href)
+              if (itemId) {
+                node.tagName = 'item'
+                node.properties.id = itemId
+                if (node.properties.href === toString(node)) {
+                  node.children[0].value = linkText
+                }
+              }
+            }
+          } catch {
+            // ignore errors like invalid URLs
+          }
+        }
+
+        // only show a link as an embed if it doesn't have text siblings
+        if (node.tagName === 'a' &&
+                  !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
+                  toString(node) === node.properties.href) {
+          const embed = parseEmbedUrl(node.properties.href)
+          if (embed) {
+            node.tagName = 'embed'
+            node.properties = { ...embed, src: node.properties.href }
+          } else {
+            node.tagName = 'autolink'
+          }
+        }
+
+        // if the link text is a URL, just show the URL
+        if (node.tagName === 'a' && isMisleadingLink(toString(node), node.properties.href)) {
+          node.children = [{ type: 'text', value: node.properties.href }]
+          return [SKIP]
+        }
+
+        // Handle @mentions and ~subs
+        if (node.type === 'text') {
+          const newChildren = []
+          let lastIndex = 0
+          let match
+          let childrenConsumed = 1
+          let text = toString(node)
+
+          const combinedRegex = new RegExp(mentionRegex.source + '|' + subRegex.source, 'gi')
+
+          // handle @__username__ or ~__sub__
+          if (['@', '~'].includes(node.value) &&
+            parent.children[index + 1]?.tagName === 'strong' &&
+            parent.children[index + 1].children[0]?.type === 'text') {
+            childrenConsumed = 2
+            text = node.value + '__' + toString(parent.children[index + 1]) + '__'
+          }
+
+          while ((match = combinedRegex.exec(text)) !== null) {
+            if (lastIndex < match.index) {
+              newChildren.push({ type: 'text', value: text.slice(lastIndex, match.index) })
+            }
+
+            const [fullMatch, mentionMatch, subMatch] = match
+            const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch) : replaceSub(fullMatch, subMatch)
+
+            if (replacement) {
+              newChildren.push(replacement)
+            } else {
+              newChildren.push({ type: 'text', value: fullMatch })
+            }
+
+            lastIndex = combinedRegex.lastIndex
+          }
+
+          if (newChildren.length > 0) {
+            if (lastIndex < text.length) {
+              newChildren.push({ type: 'text', value: text.slice(lastIndex) })
+            }
+            parent.children.splice(index, childrenConsumed, ...newChildren)
+            return index + newChildren.length
+          }
+        }
+
+        // Handle Nostr IDs
+        if (node.type === 'text') {
+          const newChildren = []
+          let lastIndex = 0
+          let match
+
+          while ((match = nostrIdRegex.exec(node.value)) !== null) {
+            if (lastIndex < match.index) {
+              newChildren.push({ type: 'text', value: node.value.slice(lastIndex, match.index) })
+            }
+
+            newChildren.push(replaceNostrId(match[0], match[0]))
+
+            lastIndex = nostrIdRegex.lastIndex
+          }
+
+          if (lastIndex < node.value.length) {
+            newChildren.push({ type: 'text', value: node.value.slice(lastIndex) })
+          }
+
+          if (newChildren.length > 0) {
+            parent.children.splice(index, 1, ...newChildren)
+            return index + newChildren.length
+          }
+        }
+
+        // handle custom tags
+        if (node.type === 'element') {
+          for (const { startTag, endTag, className } of stylers) {
+            for (let i = 0; i < node.children.length - 2; i++) {
+              const [start, text, end] = node.children.slice(i, i + 3)
+
+              if (start?.type === 'raw' && start?.value === startTag &&
+                  text?.type === 'text' &&
+                  end?.type === 'raw' && end?.value === endTag) {
+                const newChild = {
+                  type: 'element',
+                  tagName: 'span',
+                  properties: { className: [className] },
+                  children: [{ type: 'text', value: text.value }]
+                }
+                node.children.splice(i, 3, newChild)
+              }
+            }
+          }
+        }
+
+        // merge adjacent images and empty paragraphs into a single image collage
+        if ((node.tagName === 'img' || isImageOnlyParagraph(node)) && Array.isArray(parent.children)) {
+          const adjacentNodes = [node]
+          let nextIndex = index + 1
+          const siblings = parent.children
+          const somethingBefore = parent.children[index - 1] && parent.children[index - 1].tagName !== 'p'
+          let somethingAfter = false
+
+          while (nextIndex < siblings.length) {
+            const nextNode = siblings[nextIndex]
+            if (!nextNode) break
+            if (nextNode.tagName === 'img' || isImageOnlyParagraph(nextNode)) {
+              adjacentNodes.push(nextNode)
+              nextIndex++
+            } else if (nextNode.type === 'text' && typeof nextNode.value === 'string' && !nextNode.value.trim()) {
+              nextIndex++
+            } else {
+              somethingAfter = true
+              break
+            }
+          }
+
+          if (adjacentNodes.length > 0) {
+            const allImages = adjacentNodes.flatMap(n =>
+              n.tagName === 'img' ? [n] : (Array.isArray(n.children) ? n.children.filter(child => child.tagName === 'img') : [])
+            )
+            const collageNode = {
+              type: 'element',
+              tagName: 'p',
+              children: allImages,
+              properties: { onlyImages: true, somethingBefore, somethingAfter }
+            }
+            parent.children.splice(index, nextIndex - index, collageNode)
+            return index + 1
+          }
+        }
+      })
+    } catch (error) {
+      console.error('Error in rehypeSN transformer:', error)
+    }
+
+    return tree
+  }
+
+  function isImageOnlyParagraph (node) {
+    return node &&
+        node.tagName === 'p' &&
+        Array.isArray(node.children) &&
+        node.children.every(child =>
+          (child.tagName === 'img') ||
+          (child.type === 'text' && typeof child.value === 'string' && !child.value.trim())
+        )
+  }
+
+  function replaceMention (value, username) {
+    return {
+      type: 'element',
+      tagName: 'mention',
+      properties: { href: '/' + username, name: username },
+      children: [{ type: 'text', value }]
+    }
+  }
+
+  function replaceSub (value, sub) {
+    return {
+      type: 'element',
+      tagName: 'sub',
+      properties: { href: '/~' + sub, name: sub },
+      children: [{ type: 'text', value }]
+    }
+  }
+
+  function isMisleadingLink (text, href) {
+    let misleading = false
+
+    if (/^\s*(\w+\.)+\w+/.test(text)) {
+      try {
+        const hrefUrl = new URL(href)
+
+        if (new URL(hrefUrl.protocol + text).origin !== hrefUrl.origin) {
+          misleading = true
+        }
+      } catch {}
+    }
+
+    return misleading
+  }
+
+  function replaceNostrId (value, id) {
+    return {
+      type: 'element',
+      tagName: 'a',
+      properties: { href: `https://njump.me/${id}` },
+      children: [{ type: 'text', value }]
+    }
+  }
+}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 4f6e57e26..b358ea4ed 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -14,61 +14,6 @@ const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)',
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
 
-function alternateCreateDetails(markdownContent, summaryText) {
-  // Parse both summary and content as markdown
-  const mdastSummary = fromMarkdown(summaryText, {
-    extensions: [gfm()],
-    mdastExtensions: [gfmFromMarkdown()]
-  })
-
-  const mdastContent = fromMarkdown(markdownContent, {
-    extensions: [gfm()],
-    mdastExtensions: [gfmFromMarkdown()]
-  })
-
-  // Convert both to hast
-  const hastSummary = toHast(mdastSummary)
-  const hastContent = toHast(mdastContent)
-
-  // For summary, we want to ensure the content stays inline
-  // If hastSummary has block elements, we need to extract their text content
-  const flattenBlockElements = (node) => {
-    if (node.type === 'text') return [node]
-    if (!node.children) return []
-    
-    return node.children.flatMap(child => {
-      if (child.type === 'text') return [child]
-      if (child.type === 'element') {
-        // Convert block elements to spans to keep them inline
-        if (['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) {
-          return child.children.flatMap(flattenBlockElements)
-        }
-        return [child]
-      }
-      return flattenBlockElements(child)
-    })
-  }
-
-  const summaryChildren = hastSummary.children.flatMap(flattenBlockElements)
-
-  // Create the details structure
-  return {
-    type: 'element',
-    tagName: 'details',
-    properties: {},
-    children: [
-      {
-        type: 'element',
-        tagName: 'summary',
-        properties: {},
-        children: summaryChildren
-      },
-      // For the content, we want to preserve block formatting
-      ...hastContent.children
-    ]
-  }
-}
-
 export default function rehypeSN (options = {}) {
   const { stylers = [] } = options
 
@@ -77,29 +22,13 @@ export default function rehypeSN (options = {}) {
       let detailsStack = []  // Track nested details processing
 
       visit(tree, (node, index, parent) => {
-        // Log node info for debugging
-        const nodeInfo = {
-          ...(node.type && { type: node.type }),
-          ...(node.tagName && { tagName: node.tagName }),
-          ...(node.properties && { properties: node.properties }),
-          ...(node.value && { value: node.value }),
-          ...(node.children && { children: `${node.children.length} children` }),
-          ...(parent?.type && { parentType: parent.type }),
-          ...(parent?.tagName && { parentTagName: parent.tagName }),
-          index
-        }
-        console.log('Node:', nodeInfo)
-
-        // Handle details tags
+        // Handle details tags - supports both single-line and multi-line formats
         if (node.type === 'raw' && (
           node.value.includes('<details>') || 
           node.value.includes('<details >')
         )) {
-          console.log('Details opening tag found')
-          
-          // Check if this is a single-line details tag
+          // Single-line details tag handling (e.g., <details>content</details>)
           if (node.value.includes('</details>')) {
-            // Extract content between tags
             const fullContent = node.value
             let content = fullContent
               .replace(/<details>|<details >/, '')
@@ -107,31 +36,28 @@ export default function rehypeSN (options = {}) {
             
             let summaryText = 'Details'
             
-            // Check for summary tags
+            // Extract summary if present
             if (content.includes('<summary>') && content.includes('</summary>')) {
               const summaryStart = content.indexOf('<summary>') + 9
               const summaryEnd = content.indexOf('</summary>')
               summaryText = content.slice(summaryStart, summaryEnd)
               
-              // Get content before and after summary
+              // Combine content before and after summary
               const beforeSummary = content.slice(0, content.indexOf('<summary>')).trim()
               const afterSummary = content.slice(summaryEnd + 10).trim()
-              
               content = [beforeSummary, afterSummary].filter(Boolean).join('\n')
             }
             
-            // Create the details node directly
-            const newDetailsNode = alternateCreateDetails(
+            // Create and replace with new details node
+            const newDetailsNode = createDetails(
               content.trim(),
               summaryText
             )
-            
-            // Replace the current node
             parent.children[index] = newDetailsNode
             return index
           }
           
-          // Initialize details processing state for multi-line case
+          // Initialize state for multi-line details processing
           detailsStack.push({
             startIndex: index,
             contentBuffer: [],
@@ -140,53 +66,44 @@ export default function rehypeSN (options = {}) {
             foundSummary: false,
             summaryText: '',
             detailsContent: [],
-            processedNodes: new Set() // Track which nodes we've processed
+            processedNodes: new Set()
           })
-
           return
         }
 
-        // Process details content if we're inside a details tag
+        // Process multi-line details content
         if (detailsStack.length > 0) {
           const state = detailsStack[detailsStack.length - 1]
-          
-          // Skip if we've already processed this node
-          if (state.processedNodes.has(index)) {
-            return
-          }
+          if (state.processedNodes.has(index)) return
           state.processedNodes.add(index)
 
-          // Check for closing details tag
+          // Handle details closing tag
           if (node.type === 'raw' && node.value.includes('</details>')) {
-            console.log('Details closing tag found')
             detailsStack.pop()
             
-            // Get any content before the closing tag
+            // Collect any remaining content
             const beforeClosing = node.value.split('</details>')[0]
             if (beforeClosing) {
               state.contentBuffer.push(beforeClosing)
             }
 
-            // Process all collected content
+            // Process collected content
             const content = state.contentBuffer
-              .filter(Boolean) // Remove empty strings
+              .filter(Boolean)
               .join('\n')
-              .replace(/\n{3,}/g, '\n\n') // Replace 3 or more newlines with 2
+              .replace(/\n{3,}/g, '\n\n')
               .trim()
 
-            // Create the details node
             const markdownContent = state.detailsContent
               .map(node => toString(node))
               .join('\n')
 
-            const newDetailsNode = alternateCreateDetails(
+            // Create and replace with new details node
+            const newDetailsNode = createDetails(
               content || markdownContent,
               state.summaryText ? state.summaryText.trim() : 'Details'
             )
-
-            // Replace all nodes from start to current with new details node
             parent.children.splice(state.startIndex, index - state.startIndex + 1, newDetailsNode)
-            
             return state.startIndex
           }
 
@@ -195,33 +112,24 @@ export default function rehypeSN (options = {}) {
             state.inSummary = true
             state.foundSummary = true
             const parts = node.value.split('<summary>')
-            if (parts[0]) {
-              state.contentBuffer.push(parts[0])
-            }
-            if (parts[1]) {
-              state.summaryBuffer.push(parts[1])
-            }
+            if (parts[0]) state.contentBuffer.push(parts[0])
+            if (parts[1]) state.summaryBuffer.push(parts[1])
           } else if (node.type === 'raw' && node.value.includes('</summary>')) {
             state.inSummary = false
             const parts = node.value.split('</summary>')
-            if (parts[0]) {
-              state.summaryBuffer.push(parts[0])
-            }
-            if (parts[1]) {
-              state.contentBuffer.push(parts[1])
-            }
+            if (parts[0]) state.summaryBuffer.push(parts[0])
+            if (parts[1]) state.contentBuffer.push(parts[1])
             state.summaryText = state.summaryBuffer.join('\n')
           } else if (state.inSummary) {
-            // Collect summary text
+            // Collect summary content
             if (node.type === 'text' || node.type === 'raw') {
               state.summaryBuffer.push(node.value)
             } else if (node.type === 'element') {
               state.summaryBuffer.push(toString(node))
             }
           } else {
-            // Handle content collection
+            // Collect details content
             if (node.type === 'text') {
-              // Only add non-empty text nodes or preserve single newlines
               if (node.value.trim() || node.value === '\n') {
                 state.contentBuffer.push(node.value)
               }
@@ -233,7 +141,6 @@ export default function rehypeSN (options = {}) {
               state.contentBuffer.push(node.value)
             }
           }
-
           return
         }
 
@@ -494,6 +401,71 @@ export default function rehypeSN (options = {}) {
       children: [{ type: 'text', value }]
     }
   }
+
+  // Creates a details node with proper markdown parsing for both summary and content
+  function createDetails(markdownContent, summaryText) {
+    // Parse both summary and content as markdown
+    const mdastSummary = fromMarkdown(summaryText, {
+      extensions: [gfm()],
+      mdastExtensions: [gfmFromMarkdown()]
+    })
+
+    const mdastContent = fromMarkdown(markdownContent, {
+      extensions: [gfm()],
+      mdastExtensions: [gfmFromMarkdown()]
+    })
+
+    // Convert both to hast
+    const hastSummary = toHast(mdastSummary)
+    const hastContent = toHast(mdastContent)
+
+    // Ensure summary content stays inline by flattening block elements
+    const flattenBlockElements = (node) => {
+      if (node.type === 'text') return [node]
+      if (!node.children) return []
+      
+      return node.children.flatMap(child => {
+        if (child.type === 'text') return [child]
+        if (child.type === 'element') {
+          // Convert block elements to spans to keep them inline
+          if (['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) {
+            return child.children.flatMap(flattenBlockElements)
+          }
+          return [child]
+        }
+        return flattenBlockElements(child)
+      })
+    }
+
+    const summaryChildren = hastSummary.children.flatMap(flattenBlockElements)
+
+    // Create the details structure
+    return {
+      type: 'element',
+      tagName: 'details',
+      properties: {},
+      children: [
+        {
+          type: 'element',
+          tagName: 'summary',
+          properties: {},
+          children: summaryChildren
+        },
+        // Preserve block formatting in content
+        ...hastContent.children
+      ]
+    }
+  }
+
+  function isImageOnlyParagraph (node) {
+    return node &&
+        node.tagName === 'p' &&
+        Array.isArray(node.children) &&
+        node.children.every(child =>
+          (child.tagName === 'img') ||
+          (child.type === 'text' && typeof child.value === 'string' && !child.value.trim())
+        )
+  }
 }
 
 

From 8cb58741c8a8b537dce6aa30cce4153f3d900046 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 2 Dec 2024 01:58:02 -0500
Subject: [PATCH 15/23] Add minimal styling to details and summary components

---
 components/text.module.css | 38 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/components/text.module.css b/components/text.module.css
index a8a3c7bcd..9292fb5c6 100644
--- a/components/text.module.css
+++ b/components/text.module.css
@@ -433,3 +433,41 @@
     border-radius: 13px;
     overflow: hidden;
 }
+
+/* Details/Summary styling */
+.details {
+    border: 1px solid var(--theme-quoteBar);
+    border-radius: 6px;
+    padding: 0.75rem;
+    margin: calc(var(--grid-gap) * 0.5) 0;
+    background: var(--bs-body-bg);
+    transition: all 0.2s ease;
+}
+
+.details[open] {
+    background: color-mix(in srgb, var(--bs-body-bg) 97%, white);
+}
+
+.summary {
+    cursor: pointer;
+    user-select: none;
+    padding: 0.25rem 0;
+    margin: -0.25rem 0;
+    color: var(--bs-info);
+    font-weight: 500;
+}
+
+.summary:hover {
+    color: color-mix(in srgb, var(--bs-info) 85%, white);
+}
+
+.summary::marker,
+.summary::-webkit-details-marker {
+    color: var(--bs-info);
+}
+
+.details[open] > .summary {
+    margin-bottom: 0.75rem;
+    padding-bottom: 0.75rem;
+    border-bottom: 1px solid var(--theme-quoteBar);
+}

From 7bbadb97f67250b69dd54bef7696493ea8129eb2 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 2 Dec 2024 02:14:00 -0500
Subject: [PATCH 16/23] Remove old file as reference

---
 lib/rehype-sn-OLD.js | 277 -------------------------------------------
 lib/rehype-sn.js     |   4 -
 2 files changed, 281 deletions(-)
 delete mode 100644 lib/rehype-sn-OLD.js

diff --git a/lib/rehype-sn-OLD.js b/lib/rehype-sn-OLD.js
deleted file mode 100644
index fb35bf4bd..000000000
--- a/lib/rehype-sn-OLD.js
+++ /dev/null
@@ -1,277 +0,0 @@
-import { SKIP, visit } from 'unist-util-visit'
-import { parseEmbedUrl, parseInternalLinks } from './url'
-import { slug } from 'github-slugger'
-import { toString } from 'mdast-util-to-string'
-
-const userGroup = '[\\w_]+'
-const subGroup = '[A-Za-z][\\w_]+'
-
-const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
-const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
-const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
-
-export default function rehypeSN (options = {}) {
-  const { stylers = [] } = options
-
-  return function transformer (tree) {
-    try {
-      visit(tree, (node, index, parent) => {
-        // Handle inline code property
-        if (node.tagName === 'code') {
-          node.properties.inline = !(parent && parent.tagName === 'pre')
-        }
-
-        // handle headings
-        if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
-          const nodeText = toString(node)
-          const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
-          node.properties.id = headingId
-
-          // Create a new link element
-          const linkElement = {
-            type: 'element',
-            tagName: 'headlink',
-            properties: {
-              href: `#${headingId}`
-            },
-            children: [{ type: 'text', value: nodeText }]
-          }
-
-          // Replace the heading's children with the new link element
-          node.children = [linkElement]
-          return [SKIP]
-        }
-
-        // if img is wrapped in a link, remove the link
-        if (node.tagName === 'a' && node.children.length === 1 && node.children[0].tagName === 'img') {
-          parent.children[index] = node.children[0]
-          return index
-        }
-
-        // handle internal links
-        if (node.tagName === 'a') {
-          try {
-            if (node.properties.href.includes('#itemfn-')) {
-              node.tagName = 'footnote'
-            } else {
-              const { itemId, linkText } = parseInternalLinks(node.properties.href)
-              if (itemId) {
-                node.tagName = 'item'
-                node.properties.id = itemId
-                if (node.properties.href === toString(node)) {
-                  node.children[0].value = linkText
-                }
-              }
-            }
-          } catch {
-            // ignore errors like invalid URLs
-          }
-        }
-
-        // only show a link as an embed if it doesn't have text siblings
-        if (node.tagName === 'a' &&
-                  !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
-                  toString(node) === node.properties.href) {
-          const embed = parseEmbedUrl(node.properties.href)
-          if (embed) {
-            node.tagName = 'embed'
-            node.properties = { ...embed, src: node.properties.href }
-          } else {
-            node.tagName = 'autolink'
-          }
-        }
-
-        // if the link text is a URL, just show the URL
-        if (node.tagName === 'a' && isMisleadingLink(toString(node), node.properties.href)) {
-          node.children = [{ type: 'text', value: node.properties.href }]
-          return [SKIP]
-        }
-
-        // Handle @mentions and ~subs
-        if (node.type === 'text') {
-          const newChildren = []
-          let lastIndex = 0
-          let match
-          let childrenConsumed = 1
-          let text = toString(node)
-
-          const combinedRegex = new RegExp(mentionRegex.source + '|' + subRegex.source, 'gi')
-
-          // handle @__username__ or ~__sub__
-          if (['@', '~'].includes(node.value) &&
-            parent.children[index + 1]?.tagName === 'strong' &&
-            parent.children[index + 1].children[0]?.type === 'text') {
-            childrenConsumed = 2
-            text = node.value + '__' + toString(parent.children[index + 1]) + '__'
-          }
-
-          while ((match = combinedRegex.exec(text)) !== null) {
-            if (lastIndex < match.index) {
-              newChildren.push({ type: 'text', value: text.slice(lastIndex, match.index) })
-            }
-
-            const [fullMatch, mentionMatch, subMatch] = match
-            const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch) : replaceSub(fullMatch, subMatch)
-
-            if (replacement) {
-              newChildren.push(replacement)
-            } else {
-              newChildren.push({ type: 'text', value: fullMatch })
-            }
-
-            lastIndex = combinedRegex.lastIndex
-          }
-
-          if (newChildren.length > 0) {
-            if (lastIndex < text.length) {
-              newChildren.push({ type: 'text', value: text.slice(lastIndex) })
-            }
-            parent.children.splice(index, childrenConsumed, ...newChildren)
-            return index + newChildren.length
-          }
-        }
-
-        // Handle Nostr IDs
-        if (node.type === 'text') {
-          const newChildren = []
-          let lastIndex = 0
-          let match
-
-          while ((match = nostrIdRegex.exec(node.value)) !== null) {
-            if (lastIndex < match.index) {
-              newChildren.push({ type: 'text', value: node.value.slice(lastIndex, match.index) })
-            }
-
-            newChildren.push(replaceNostrId(match[0], match[0]))
-
-            lastIndex = nostrIdRegex.lastIndex
-          }
-
-          if (lastIndex < node.value.length) {
-            newChildren.push({ type: 'text', value: node.value.slice(lastIndex) })
-          }
-
-          if (newChildren.length > 0) {
-            parent.children.splice(index, 1, ...newChildren)
-            return index + newChildren.length
-          }
-        }
-
-        // handle custom tags
-        if (node.type === 'element') {
-          for (const { startTag, endTag, className } of stylers) {
-            for (let i = 0; i < node.children.length - 2; i++) {
-              const [start, text, end] = node.children.slice(i, i + 3)
-
-              if (start?.type === 'raw' && start?.value === startTag &&
-                  text?.type === 'text' &&
-                  end?.type === 'raw' && end?.value === endTag) {
-                const newChild = {
-                  type: 'element',
-                  tagName: 'span',
-                  properties: { className: [className] },
-                  children: [{ type: 'text', value: text.value }]
-                }
-                node.children.splice(i, 3, newChild)
-              }
-            }
-          }
-        }
-
-        // merge adjacent images and empty paragraphs into a single image collage
-        if ((node.tagName === 'img' || isImageOnlyParagraph(node)) && Array.isArray(parent.children)) {
-          const adjacentNodes = [node]
-          let nextIndex = index + 1
-          const siblings = parent.children
-          const somethingBefore = parent.children[index - 1] && parent.children[index - 1].tagName !== 'p'
-          let somethingAfter = false
-
-          while (nextIndex < siblings.length) {
-            const nextNode = siblings[nextIndex]
-            if (!nextNode) break
-            if (nextNode.tagName === 'img' || isImageOnlyParagraph(nextNode)) {
-              adjacentNodes.push(nextNode)
-              nextIndex++
-            } else if (nextNode.type === 'text' && typeof nextNode.value === 'string' && !nextNode.value.trim()) {
-              nextIndex++
-            } else {
-              somethingAfter = true
-              break
-            }
-          }
-
-          if (adjacentNodes.length > 0) {
-            const allImages = adjacentNodes.flatMap(n =>
-              n.tagName === 'img' ? [n] : (Array.isArray(n.children) ? n.children.filter(child => child.tagName === 'img') : [])
-            )
-            const collageNode = {
-              type: 'element',
-              tagName: 'p',
-              children: allImages,
-              properties: { onlyImages: true, somethingBefore, somethingAfter }
-            }
-            parent.children.splice(index, nextIndex - index, collageNode)
-            return index + 1
-          }
-        }
-      })
-    } catch (error) {
-      console.error('Error in rehypeSN transformer:', error)
-    }
-
-    return tree
-  }
-
-  function isImageOnlyParagraph (node) {
-    return node &&
-        node.tagName === 'p' &&
-        Array.isArray(node.children) &&
-        node.children.every(child =>
-          (child.tagName === 'img') ||
-          (child.type === 'text' && typeof child.value === 'string' && !child.value.trim())
-        )
-  }
-
-  function replaceMention (value, username) {
-    return {
-      type: 'element',
-      tagName: 'mention',
-      properties: { href: '/' + username, name: username },
-      children: [{ type: 'text', value }]
-    }
-  }
-
-  function replaceSub (value, sub) {
-    return {
-      type: 'element',
-      tagName: 'sub',
-      properties: { href: '/~' + sub, name: sub },
-      children: [{ type: 'text', value }]
-    }
-  }
-
-  function isMisleadingLink (text, href) {
-    let misleading = false
-
-    if (/^\s*(\w+\.)+\w+/.test(text)) {
-      try {
-        const hrefUrl = new URL(href)
-
-        if (new URL(hrefUrl.protocol + text).origin !== hrefUrl.origin) {
-          misleading = true
-        }
-      } catch {}
-    }
-
-    return misleading
-  }
-
-  function replaceNostrId (value, id) {
-    return {
-      type: 'element',
-      tagName: 'a',
-      properties: { href: `https://njump.me/${id}` },
-      children: [{ type: 'text', value }]
-    }
-  }
-}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index d7fbeffb5..f7d5baf38 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -4,10 +4,8 @@ import { slug } from 'github-slugger'
 import { toString } from 'mdast-util-to-string'
 import { fromMarkdown } from 'mdast-util-from-markdown'
 import { toHast } from 'mdast-util-to-hast'
-import { fromMarkdown } from 'mdast-util-from-markdown'
 import { gfm } from 'micromark-extension-gfm'
 import { gfmFromMarkdown } from 'mdast-util-gfm'
-import { toHast } from 'mdast-util-to-hast'
 
 const userGroup = '[\\w_]+'
 const subGroup = '[A-Za-z][\\w_]+'
@@ -15,8 +13,6 @@ const subGroup = '[A-Za-z][\\w_]+'
 const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
-const detailsRegex = /<details>([\s\S]*?)<\/details>/
-const summaryRegex = /<summary>([\s\S]*?)<\/summary>/
 
 export default function rehypeSN (options = {}) {
   const { stylers = [] } = options

From 77428a9317da9fcc146daef52676e6a34deaa3c4 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 2 Dec 2024 02:27:21 -0500
Subject: [PATCH 17/23] Linting & remove isOpen state from Details component
 (already handled)

---
 components/text.js | 21 +++--------
 lib/rehype-sn.js   | 88 +++++++---------------------------------------
 2 files changed, 18 insertions(+), 91 deletions(-)

diff --git a/components/text.js b/components/text.js
index dfa0d9b3a..6e72095d4 100644
--- a/components/text.js
+++ b/components/text.js
@@ -127,9 +127,6 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
     embed: Embed,
     details: Details,
     summary: Summary
-    embed: Embed,
-    details: Details,
-    summary: Summary,
   }), [outlawed, rel, TextMediaOrLink, topLevel])
 
   const carousel = useCarousel()
@@ -228,7 +225,6 @@ function Table ({ node, ...props }) {
   )
 }
 
-
 function Code ({ node, inline, className, children, style, ...props }) {
   return inline
     ? (
@@ -254,25 +250,18 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
   )
 }
 
-function Details({ children, node, ...props }) {
-  const [isOpen, setIsOpen] = useState(false)
-
-  const handleToggle = (e) => {
-    setIsOpen(e.target.open)
-  }
-
+function Details ({ children, node, ...props }) {
   return (
-    <details 
-      className={styles.details} 
-      {...props} 
-      onToggle={handleToggle}
+    <details
+      className={styles.details}
+      {...props}
     >
       {children}
     </details>
   )
 }
 
-function Summary({ children, node, ...props }) {
+function Summary ({ children, node, ...props }) {
   return (
     <summary className={styles.summary} {...props}>
       {children}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index f7d5baf38..ffe3a9f95 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -19,12 +19,12 @@ export default function rehypeSN (options = {}) {
 
   return function transformer (tree) {
     try {
-      let detailsStack = []  // Track nested details processing
+      const detailsStack = [] // Track nested details processing
 
       visit(tree, (node, index, parent) => {
         // Handle details tags - supports both single-line and multi-line formats
         if (node.type === 'raw' && (
-          node.value.includes('<details>') || 
+          node.value.includes('<details>') ||
           node.value.includes('<details >')
         )) {
           // Single-line details tag handling (e.g., <details>content</details>)
@@ -33,21 +33,21 @@ export default function rehypeSN (options = {}) {
             let content = fullContent
               .replace(/<details>|<details >/, '')
               .replace('</details>', '')
-            
+
             let summaryText = 'Details'
-            
+
             // Extract summary if present
             if (content.includes('<summary>') && content.includes('</summary>')) {
               const summaryStart = content.indexOf('<summary>') + 9
               const summaryEnd = content.indexOf('</summary>')
               summaryText = content.slice(summaryStart, summaryEnd)
-              
+
               // Combine content before and after summary
               const beforeSummary = content.slice(0, content.indexOf('<summary>')).trim()
               const afterSummary = content.slice(summaryEnd + 10).trim()
               content = [beforeSummary, afterSummary].filter(Boolean).join('\n')
             }
-            
+
             // Create and replace with new details node
             const newDetailsNode = createDetails(
               content.trim(),
@@ -56,7 +56,7 @@ export default function rehypeSN (options = {}) {
             parent.children[index] = newDetailsNode
             return index
           }
-          
+
           // Initialize state for multi-line details processing
           detailsStack.push({
             startIndex: index,
@@ -80,7 +80,7 @@ export default function rehypeSN (options = {}) {
           // Handle details closing tag
           if (node.type === 'raw' && node.value.includes('</details>')) {
             detailsStack.pop()
-            
+
             // Collect any remaining content
             const beforeClosing = node.value.split('</details>')[0]
             if (beforeClosing) {
@@ -135,9 +135,9 @@ export default function rehypeSN (options = {}) {
               }
             } else if (node.type === 'element') {
               state.contentBuffer.push(toString(node))
-            } else if (node.type === 'raw' && 
-                       !node.value.includes('<details>') &&
-                       !node.value.includes('</details>')) {
+            } else if (node.type === 'raw' &&
+                      !node.value.includes('<details>') &&
+                      !node.value.includes('</details>')) {
               state.contentBuffer.push(node.value)
             }
           }
@@ -403,7 +403,7 @@ export default function rehypeSN (options = {}) {
   }
 
   // Creates a details node with proper markdown parsing for both summary and content
-  function createDetails(markdownContent, summaryText) {
+  function createDetails (markdownContent, summaryText) {
     // Parse both summary and content as markdown
     const mdastSummary = fromMarkdown(summaryText, {
       extensions: [gfm()],
@@ -423,7 +423,7 @@ export default function rehypeSN (options = {}) {
     const flattenBlockElements = (node) => {
       if (node.type === 'text') return [node]
       if (!node.children) return []
-      
+
       return node.children.flatMap(child => {
         if (child.type === 'text') return [child]
         if (child.type === 'element') {
@@ -456,66 +456,4 @@ export default function rehypeSN (options = {}) {
       ]
     }
   }
-
-  function isImageOnlyParagraph (node) {
-    return node &&
-        node.tagName === 'p' &&
-        Array.isArray(node.children) &&
-        node.children.every(child =>
-          (child.tagName === 'img') ||
-          (child.type === 'text' && typeof child.value === 'string' && !child.value.trim())
-        )
-  }
 }
-
-
-// Different structures that must be handled properly and not break details/summary
-
-// Structure 1:
-
-// <details>no summary tags
-// lorem ipsum
-// and text on a new line</details>
-
-// ----------
-
-// Structure 2:
-
-// <details>
-// <summary>text inside summary</summary>
-
-// lorem ipsum
-// </details>
-
-// ----------
-
-// Structure 3:
-
-// <details>
-//     <summary>summary indentation single line</summary>
-// 1. first thing
-// 2. second thing 
-// 3. third thing
-// </details>
-
-// ----------
-
-// Structure 4:
-
-// <details>
-//   <summary>
-//     summary text here
-//   </summary>
-//   text inside details with *markdown* working **properly**
-// </details>
-
-// ----------
-
-// Structure 5:
-
-// <details>
-// <summary>Shopping list</summary>
-// - Vegetables
-// - Fruits
-// - Fish
-// </details>

From f19fce26a7465ecdac7ae3c267f9ba85f27434c7 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 16 Dec 2024 00:23:31 -0500
Subject: [PATCH 18/23] Structure 1 & 1A perfect beheavior

---
 components/text.js         |   9 +-
 components/text.module.css |  36 ---
 lib/rehype-sn.js           | 510 ++++++++++++++++++++++---------------
 3 files changed, 304 insertions(+), 251 deletions(-)

diff --git a/components/text.js b/components/text.js
index d38431495..36b06cf0e 100644
--- a/components/text.js
+++ b/components/text.js
@@ -255,18 +255,15 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
   )
 }
 
-function Details ({ children, node, ...props }) {
+function Details({ children, node, ...props }) {
   return (
-    <details
-      className={styles.details}
-      {...props}
-    >
+    <details className={styles.details} {...props}>
       {children}
     </details>
   )
 }
 
-function Summary ({ children, node, ...props }) {
+function Summary({ children, node, ...props }) {
   return (
     <summary className={styles.summary} {...props}>
       {children}
diff --git a/components/text.module.css b/components/text.module.css
index 82f6535de..b526da1ac 100644
--- a/components/text.module.css
+++ b/components/text.module.css
@@ -439,39 +439,3 @@
 }
 
 /* Details/Summary styling */
-.details {
-    border: 1px solid var(--theme-quoteBar);
-    border-radius: 6px;
-    padding: 0.75rem;
-    margin: calc(var(--grid-gap) * 0.5) 0;
-    background: var(--bs-body-bg);
-    transition: all 0.2s ease;
-}
-
-.details[open] {
-    background: color-mix(in srgb, var(--bs-body-bg) 97%, white);
-}
-
-.summary {
-    cursor: pointer;
-    user-select: none;
-    padding: 0.25rem 0;
-    margin: -0.25rem 0;
-    color: var(--bs-info);
-    font-weight: 500;
-}
-
-.summary:hover {
-    color: color-mix(in srgb, var(--bs-info) 85%, white);
-}
-
-.summary::marker,
-.summary::-webkit-details-marker {
-    color: var(--bs-info);
-}
-
-.details[open] > .summary {
-    margin-bottom: 0.75rem;
-    padding-bottom: 0.75rem;
-    border-bottom: 1px solid var(--theme-quoteBar);
-}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index ffe3a9f95..dcb791499 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -9,145 +9,254 @@ import { gfmFromMarkdown } from 'mdast-util-gfm'
 
 const userGroup = '[\\w_]+'
 const subGroup = '[A-Za-z][\\w_]+'
-
 const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)', 'gi')
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
 
-export default function rehypeSN (options = {}) {
+// Helper to safely stringify node content
+function safeStringify(obj, depth = 0) {
+  if (depth > 2) return '[Nested Object]' // Prevent infinite recursion
+  try {
+    return JSON.stringify(obj, (key, value) => {
+      if (key === 'parent') return '[Parent]' // Skip circular parent refs
+      if (typeof value === 'object' && value !== null && depth < 2) {
+        return Object.fromEntries(
+          Object.entries(value).map(([k, v]) => [k, safeStringify(v, depth + 1)])
+        )
+      }
+      return value
+    }, 2)
+  } catch (e) {
+    return String(obj)
+  }
+}
+
+// Helper to print node info
+function logNode(prefix, node, detailed = false) {
+  const nodeInfo = {
+    type: node.type,
+    tagName: node.tagName,
+    childCount: node.children?.length,
+    value: node.type === 'text' || node.type === 'raw' 
+      ? node.value.substring(0, 100) + (node.value.length > 100 ? '...' : '')
+      : undefined,
+    properties: node.properties
+  }
+  
+  console.log(`${prefix} Node:`, safeStringify(nodeInfo))
+  
+  if (detailed && node.children?.length > 0) {
+    console.log(`${prefix} Children:`)
+    node.children.forEach((child, i) => {
+      logNode(`${prefix}   [${i}]`, child)
+    })
+  }
+}
+
+export default function rehypeSN(options = {}) {
   const { stylers = [] } = options
 
-  return function transformer (tree) {
+  return function transformer(tree) {
     try {
-      const detailsStack = [] // Track nested details processing
-
       visit(tree, (node, index, parent) => {
-        // Handle details tags - supports both single-line and multi-line formats
-        if (node.type === 'raw' && (
-          node.value.includes('<details>') ||
-          node.value.includes('<details >')
-        )) {
-          // Single-line details tag handling (e.g., <details>content</details>)
-          if (node.value.includes('</details>')) {
-            const fullContent = node.value
-            let content = fullContent
-              .replace(/<details>|<details >/, '')
-              .replace('</details>', '')
-
-            let summaryText = 'Details'
+        // Handle details/summary tags
+        if (node.type === 'raw' && node.value.includes('<details>')) {
+          console.log('\nšŸ” Found details tag at index:', index)
+          logNode('šŸ“Œ Details', node, true)
+          
+          const detailsContent = {
+            summary: {
+              content: [],
+              found: false,
+              complete: false
+            },
+            content: [],
+            startIndex: index,
+            endIndex: index
+          }
 
+          // Handle self-contained details block
+          if (node.value.includes('</details>')) {
+            console.log('šŸ“ Found self-contained details block')
+            let content = node.value
+            
             // Extract summary if present
-            if (content.includes('<summary>') && content.includes('</summary>')) {
-              const summaryStart = content.indexOf('<summary>') + 9
-              const summaryEnd = content.indexOf('</summary>')
-              summaryText = content.slice(summaryStart, summaryEnd)
-
-              // Combine content before and after summary
-              const beforeSummary = content.slice(0, content.indexOf('<summary>')).trim()
-              const afterSummary = content.slice(summaryEnd + 10).trim()
-              content = [beforeSummary, afterSummary].filter(Boolean).join('\n')
+            const summaryMatch = content.match(/<summary>(.*?)<\/summary>/s)
+            if (summaryMatch) {
+              detailsContent.summary.content.push({
+                type: 'text',
+                value: summaryMatch[1].trim()
+              })
+              detailsContent.summary.complete = true
+              content = content.replace(/<summary>.*?<\/summary>/s, '')
+            }
+            
+            // Clean remaining content
+            const cleanedContent = content
+              .replace(/<details>/g, '')
+              .replace(/<\/details>/g, '')
+              .trim()
+            
+            if (cleanedContent) {
+              console.log('šŸ“ Keeping content from self-contained block:', cleanedContent)
+              detailsContent.content.push({
+                type: 'text',
+                value: cleanedContent
+              })
             }
 
-            // Create and replace with new details node
-            const newDetailsNode = createDetails(
-              content.trim(),
-              summaryText
-            )
-            parent.children[index] = newDetailsNode
-            return index
+            return createDetailsElement(detailsContent, parent, index)
           }
 
-          // Initialize state for multi-line details processing
-          detailsStack.push({
-            startIndex: index,
-            contentBuffer: [],
-            summaryBuffer: [],
-            inSummary: false,
-            foundSummary: false,
-            summaryText: '',
-            detailsContent: [],
-            processedNodes: new Set()
-          })
-          return
-        }
-
-        // Process multi-line details content
-        if (detailsStack.length > 0) {
-          const state = detailsStack[detailsStack.length - 1]
-          if (state.processedNodes.has(index)) return
-          state.processedNodes.add(index)
-
-          // Handle details closing tag
-          if (node.type === 'raw' && node.value.includes('</details>')) {
-            detailsStack.pop()
+          // Clean opening details tag and handle potential summary
+          let cleanedContent = node.value.replace(/<details>/g, '')
+          
+          // Check for summary in opening node
+          const summaryMatch = cleanedContent.match(/<summary>(.*?)<\/summary>/s)
+          if (summaryMatch) {
+            detailsContent.summary.content.push({
+              type: 'text',
+              value: summaryMatch[1].trim()
+            })
+            detailsContent.summary.complete = true
+            cleanedContent = cleanedContent.replace(/<summary>.*?<\/summary>/s, '')
+          }
+          
+          if (cleanedContent.trim()) {
+            console.log('šŸ“ Keeping content from opening tag node:', cleanedContent)
+            detailsContent.content.push({
+              type: 'text',
+              value: cleanedContent.trim()
+            })
+          }
 
-            // Collect any remaining content
-            const beforeClosing = node.value.split('</details>')[0]
-            if (beforeClosing) {
-              state.contentBuffer.push(beforeClosing)
+          // Collect remaining content
+          console.log('\nšŸ“ Starting content collection...')
+          let currentIndex = index
+          let foundClosing = false
+          
+          while (currentIndex < parent.children.length) {
+            const currentNode = parent.children[++currentIndex]
+            if (!currentNode) break
+            
+            console.log(`\nšŸ‘€ Examining node at index ${currentIndex}:`)
+            logNode('   ', currentNode)
+            
+            // Handle summary tags if we haven't found a complete summary yet
+            if (!detailsContent.summary.complete) {
+              if (currentNode.type === 'raw' && currentNode.value.includes('<summary>')) {
+                detailsContent.summary.found = true
+                const summaryMatch = currentNode.value.match(/<summary>(.*?)<\/summary>/s)
+                if (summaryMatch) {
+                  // Complete summary found in one node
+                  detailsContent.summary.content.push({
+                    type: 'text',
+                    value: summaryMatch[1].trim()
+                  })
+                  detailsContent.summary.complete = true
+                  continue
+                }
+                // If no match, it means the summary continues in next nodes
+                const afterOpen = currentNode.value.replace(/<summary>/g, '').trim()
+                if (afterOpen) {
+                  detailsContent.summary.content.push({
+                    type: 'text',
+                    value: afterOpen
+                  })
+                }
+                continue
+              }
+              
+              // If we're collecting summary content
+              if (detailsContent.summary.found) {
+                if (currentNode.type === 'raw' && currentNode.value.includes('</summary>')) {
+                  const beforeClose = currentNode.value.replace(/<\/summary>/g, '').trim()
+                  if (beforeClose) {
+                    detailsContent.summary.content.push({
+                      type: 'text',
+                      value: beforeClose
+                    })
+                  }
+                  detailsContent.summary.complete = true
+                  continue
+                }
+                // Add to summary content
+                if (currentNode.type === 'text' || currentNode.type === 'element') {
+                  detailsContent.summary.content.push(currentNode)
+                  continue
+                }
+              }
             }
 
-            // Process collected content
-            const content = state.contentBuffer
-              .filter(Boolean)
-              .join('\n')
-              .replace(/\n{3,}/g, '\n\n')
-              .trim()
+            // Check for closing details tag
+            const hasClosingTag = (currentNode.type === 'raw' && currentNode.value.includes('</details>')) ||
+                                (currentNode.type === 'element' && toString(currentNode).includes('</details>'))
+            
+            if (hasClosingTag) {
+              let cleanedContent
+              if (currentNode.type === 'raw') {
+                const textBeforeClosing = currentNode.value.substring(0, currentNode.value.indexOf('</details>'))
+                if (textBeforeClosing.includes('\n')) {
+                  // Parse as markdown
+                  const mdast = fromMarkdown(textBeforeClosing, {
+                    extensions: [gfm()],
+                    mdastExtensions: [gfmFromMarkdown()]
+                  })
+                  // Convert to hast
+                  const hast = toHast(mdast)
+                  // Add all children from the parsed content
+                  if (hast && hast.children) {
+                    detailsContent.content.push(...hast.children)
+                  }
+                } else {
+                  // Single line, keep as text node
+                  if (textBeforeClosing.trim()) {
+                    detailsContent.content.push({
+                      type: 'text',
+                      value: textBeforeClosing.trim()
+                    })
+                  }
+                }
+              } else {
+                // Handle element nodes similarly
+                const content = toString(currentNode).replace(/<\/details>/g, '')
+                if (content.trim()) {
+                  const mdast = fromMarkdown(content, {
+                    extensions: [gfm()],
+                    mdastExtensions: [gfmFromMarkdown()]
+                  })
+                  const hast = toHast(mdast)
+                  if (hast && hast.children) {
+                    detailsContent.content.push(...hast.children)
+                  }
+                }
+              }
 
-            const markdownContent = state.detailsContent
-              .map(node => toString(node))
-              .join('\n')
+              console.log('āœ… Found closing details tag')
+              detailsContent.endIndex = currentIndex
+              foundClosing = true
+              break
+            }
 
-            // Create and replace with new details node
-            const newDetailsNode = createDetails(
-              content || markdownContent,
-              state.summaryText ? state.summaryText.trim() : 'Details'
-            )
-            parent.children.splice(state.startIndex, index - state.startIndex + 1, newDetailsNode)
-            return state.startIndex
+            // Add to main content if not part of summary
+            if (currentNode.type === 'text' || currentNode.type === 'element') {
+              detailsContent.content.push(currentNode)
+            }
           }
 
-          // Handle summary tags
-          if (node.type === 'raw' && node.value.includes('<summary>')) {
-            state.inSummary = true
-            state.foundSummary = true
-            const parts = node.value.split('<summary>')
-            if (parts[0]) state.contentBuffer.push(parts[0])
-            if (parts[1]) state.summaryBuffer.push(parts[1])
-          } else if (node.type === 'raw' && node.value.includes('</summary>')) {
-            state.inSummary = false
-            const parts = node.value.split('</summary>')
-            if (parts[0]) state.summaryBuffer.push(parts[0])
-            if (parts[1]) state.contentBuffer.push(parts[1])
-            state.summaryText = state.summaryBuffer.join('\n')
-          } else if (state.inSummary) {
-            // Collect summary content
-            if (node.type === 'text' || node.type === 'raw') {
-              state.summaryBuffer.push(node.value)
-            } else if (node.type === 'element') {
-              state.summaryBuffer.push(toString(node))
-            }
-          } else {
-            // Collect details content
-            if (node.type === 'text') {
-              if (node.value.trim() || node.value === '\n') {
-                state.contentBuffer.push(node.value)
-              }
-            } else if (node.type === 'element') {
-              state.contentBuffer.push(toString(node))
-            } else if (node.type === 'raw' &&
-                      !node.value.includes('<details>') &&
-                      !node.value.includes('</details>')) {
-              state.contentBuffer.push(node.value)
-            }
+          if (!foundClosing) {
+            console.log('āš ļø No closing tag found, skipping...')
+            return SKIP
           }
-          return
+
+          return createDetailsElement(detailsContent, parent, index)
         }
 
-        // Handle inline code property
+        // Leave all other existing handlers unchanged
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
         }
+
         // handle headings
         if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
           const nodeText = toString(node)
@@ -195,8 +304,8 @@ export default function rehypeSN (options = {}) {
 
         // only show a link as an embed if it doesn't have text siblings
         if (node.tagName === 'a' &&
-                  !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
-                  toString(node) === node.properties.href) {
+            !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
+            toString(node) === node.properties.href) {
           const embed = parseEmbedUrl(node.properties.href)
           if (embed) {
             node.tagName = 'embed'
@@ -224,8 +333,8 @@ export default function rehypeSN (options = {}) {
 
           // handle @__username__ or ~__sub__
           if (['@', '~'].includes(node.value) &&
-            parent.children[index + 1]?.tagName === 'strong' &&
-            parent.children[index + 1].children[0]?.type === 'text') {
+              parent.children[index + 1]?.tagName === 'strong' &&
+              parent.children[index + 1].children[0]?.type === 'text') {
             childrenConsumed = 2
             text = node.value + '__' + toString(parent.children[index + 1]) + '__'
           }
@@ -268,7 +377,6 @@ export default function rehypeSN (options = {}) {
             }
 
             newChildren.push(replaceNostrId(match[0], match[0]))
-
             lastIndex = nostrIdRegex.lastIndex
           }
 
@@ -344,116 +452,100 @@ export default function rehypeSN (options = {}) {
 
       return tree
     } catch (error) {
-      console.error('Error in rehypeSN transformer:', error)
+      console.error('āŒ Error in rehypeSN transformer:', error)
       return tree
     }
   }
+}
 
-  function isImageOnlyParagraph (node) {
-    return node &&
-        node.tagName === 'p' &&
-        Array.isArray(node.children) &&
-        node.children.every(child =>
-          (child.tagName === 'img') ||
-          (child.type === 'text' && typeof child.value === 'string' && !child.value.trim())
-        )
+function isImageOnlyParagraph(node) {
+  return node &&
+    node.tagName === 'p' &&
+    Array.isArray(node.children) &&
+    node.children.every(child =>
+      (child.tagName === 'img') ||
+      (child.type === 'text' && typeof child.value === 'string' && !child.value.trim())
+    )
+}
+
+function replaceMention(value, username) {
+  return {
+    type: 'element',
+    tagName: 'mention',
+    properties: { href: '/' + username, name: username },
+    children: [{ type: 'text', value }]
   }
+}
 
-  function replaceMention (value, username) {
-    return {
-      type: 'element',
-      tagName: 'mention',
-      properties: { href: '/' + username, name: username },
-      children: [{ type: 'text', value }]
-    }
+function replaceSub(value, sub) {
+  return {
+    type: 'element',
+    tagName: 'sub',
+    properties: { href: '/~' + sub, name: sub },
+    children: [{ type: 'text', value }]
   }
+}
 
-  function replaceSub (value, sub) {
-    return {
-      type: 'element',
-      tagName: 'sub',
-      properties: { href: '/~' + sub, name: sub },
-      children: [{ type: 'text', value }]
-    }
+function replaceNostrId(value, id) {
+  return {
+    type: 'element',
+    tagName: 'a',
+    properties: { href: `https://njump.me/${id}` },
+    children: [{ type: 'text', value }]
   }
+}
 
-  function isMisleadingLink (text, href) {
-    let misleading = false
+function isMisleadingLink(text, href) {
+  let misleading = false
 
-    if (/^\s*(\w+\.)+\w+/.test(text)) {
-      try {
-        const hrefUrl = new URL(href)
+  if (/^\s*(\w+\.)+\w+/.test(text)) {
+    try {
+      const hrefUrl = new URL(href)
+      if (new URL(hrefUrl.protocol + text).origin !== hrefUrl.origin) {
+        misleading = true
+      }
+    } catch {}
+  }
 
-        if (new URL(hrefUrl.protocol + text).origin !== hrefUrl.origin) {
-          misleading = true
-        }
-      } catch {}
-    }
+  return misleading
+}
 
-    return misleading
+// Helper to create details element
+function createDetailsElement(detailsContent, parent, index) {
+  console.log('\nšŸ”Ø Creating details element with:', {
+    hasSummary: detailsContent.summary.complete,
+    contentNodes: detailsContent.content.length
+  })
+
+  const detailsElement = {
+    type: 'element',
+    tagName: 'details',
+    properties: {},
+    children: []
   }
 
-  function replaceNostrId (value, id) {
-    return {
+  // Add summary if found
+  if (detailsContent.summary.complete) {
+    const summaryElement = {
       type: 'element',
-      tagName: 'a',
-      properties: { href: `https://njump.me/${id}` },
-      children: [{ type: 'text', value }]
+      tagName: 'summary',
+      properties: {},
+      children: detailsContent.summary.content
     }
+    detailsElement.children.push(summaryElement)
+    console.log('✨ Added summary element')
   }
 
-  // Creates a details node with proper markdown parsing for both summary and content
-  function createDetails (markdownContent, summaryText) {
-    // Parse both summary and content as markdown
-    const mdastSummary = fromMarkdown(summaryText, {
-      extensions: [gfm()],
-      mdastExtensions: [gfmFromMarkdown()]
-    })
+  // Add main content
+  detailsElement.children.push(...detailsContent.content)
+  console.log('✨ Added content elements')
 
-    const mdastContent = fromMarkdown(markdownContent, {
-      extensions: [gfm()],
-      mdastExtensions: [gfmFromMarkdown()]
-    })
+  // Replace nodes
+  parent.children.splice(
+    detailsContent.startIndex,
+    detailsContent.endIndex - detailsContent.startIndex + 1,
+    detailsElement
+  )
 
-    // Convert both to hast
-    const hastSummary = toHast(mdastSummary)
-    const hastContent = toHast(mdastContent)
-
-    // Ensure summary content stays inline by flattening block elements
-    const flattenBlockElements = (node) => {
-      if (node.type === 'text') return [node]
-      if (!node.children) return []
-
-      return node.children.flatMap(child => {
-        if (child.type === 'text') return [child]
-        if (child.type === 'element') {
-          // Convert block elements to spans to keep them inline
-          if (['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) {
-            return child.children.flatMap(flattenBlockElements)
-          }
-          return [child]
-        }
-        return flattenBlockElements(child)
-      })
-    }
-
-    const summaryChildren = hastSummary.children.flatMap(flattenBlockElements)
-
-    // Create the details structure
-    return {
-      type: 'element',
-      tagName: 'details',
-      properties: {},
-      children: [
-        {
-          type: 'element',
-          tagName: 'summary',
-          properties: {},
-          children: summaryChildren
-        },
-        // Preserve block formatting in content
-        ...hastContent.children
-      ]
-    }
-  }
-}
+  return [SKIP, detailsContent.endIndex]
+}
\ No newline at end of file

From 7415f7d20e1b86544aa6aeb5c7115629723a0485 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Mon, 16 Dec 2024 01:06:58 -0500
Subject: [PATCH 19/23] Structures 1, 1A, 2, 2A perfect behavior

---
 lib/rehype-sn.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 67 insertions(+)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index dcb791499..52d8d1e9d 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -83,6 +83,10 @@ export default function rehypeSN(options = {}) {
             // Extract summary if present
             const summaryMatch = content.match(/<summary>(.*?)<\/summary>/s)
             if (summaryMatch) {
+              console.log('šŸ“Œ Found summary in self-contained block:', {
+                fullMatch: summaryMatch[0],
+                content: summaryMatch[1].trim()
+              })
               detailsContent.summary.content.push({
                 type: 'text',
                 value: summaryMatch[1].trim()
@@ -114,6 +118,10 @@ export default function rehypeSN(options = {}) {
           // Check for summary in opening node
           const summaryMatch = cleanedContent.match(/<summary>(.*?)<\/summary>/s)
           if (summaryMatch) {
+            console.log('\nšŸ“Œ Found summary in opening details node:', {
+              fullMatch: summaryMatch[0],
+              content: summaryMatch[1].trim()
+            })
             detailsContent.summary.content.push({
               type: 'text',
               value: summaryMatch[1].trim()
@@ -145,20 +153,49 @@ export default function rehypeSN(options = {}) {
             // Handle summary tags if we haven't found a complete summary yet
             if (!detailsContent.summary.complete) {
               if (currentNode.type === 'raw' && currentNode.value.includes('<summary>')) {
+                console.log('\nšŸ“Œ Found summary tag in node:', {
+                  type: currentNode.type,
+                  value: currentNode.value
+                })
                 detailsContent.summary.found = true
                 const summaryMatch = currentNode.value.match(/<summary>(.*?)<\/summary>/s)
                 if (summaryMatch) {
+                  // Keep any text that appears before the summary tag
+                  const beforeSummary = currentNode.value.substring(0, currentNode.value.indexOf('<summary>')).trim()
+                  if (beforeSummary) {
+                    console.log('šŸ“ Keeping content before summary tag:', beforeSummary)
+                    detailsContent.content.push({
+                      type: 'text',
+                      value: beforeSummary
+                    })
+                  }
+
                   // Complete summary found in one node
+                  console.log('šŸ“„ Extracted summary content:', summaryMatch[1].trim())
                   detailsContent.summary.content.push({
                     type: 'text',
                     value: summaryMatch[1].trim()
                   })
                   detailsContent.summary.complete = true
+
+                  // Preserve text after the closing summary tag
+                  const afterSummary = currentNode.value.substring(
+                    currentNode.value.indexOf('</summary>') + '</summary>'.length
+                  ).trim()
+                  
+                  if (afterSummary) {
+                    console.log('šŸ“ Keeping content after summary tag:', afterSummary)
+                    detailsContent.content.push({
+                      type: 'text',
+                      value: afterSummary
+                    })
+                  }
                   continue
                 }
                 // If no match, it means the summary continues in next nodes
                 const afterOpen = currentNode.value.replace(/<summary>/g, '').trim()
                 if (afterOpen) {
+                  console.log('šŸ“ Found partial summary content:', afterOpen)
                   detailsContent.summary.content.push({
                     type: 'text',
                     value: afterOpen
@@ -172,6 +209,7 @@ export default function rehypeSN(options = {}) {
                 if (currentNode.type === 'raw' && currentNode.value.includes('</summary>')) {
                   const beforeClose = currentNode.value.replace(/<\/summary>/g, '').trim()
                   if (beforeClose) {
+                    console.log('šŸ“ Found closing summary content:', beforeClose)
                     detailsContent.summary.content.push({
                       type: 'text',
                       value: beforeClose
@@ -182,6 +220,10 @@ export default function rehypeSN(options = {}) {
                 }
                 // Add to summary content
                 if (currentNode.type === 'text' || currentNode.type === 'element') {
+                  console.log('šŸ“ Adding summary node:', {
+                    type: currentNode.type,
+                    content: currentNode.type === 'text' ? currentNode.value : '[element]'
+                  })
                   detailsContent.summary.content.push(currentNode)
                   continue
                 }
@@ -249,6 +291,31 @@ export default function rehypeSN(options = {}) {
             return SKIP
           }
 
+          // Add comprehensive logging of collected content
+          console.log('\nšŸ“¦ Final collected content:', {
+            summary: {
+              complete: detailsContent.summary.complete,
+              nodeCount: detailsContent.summary.content.length,
+              nodes: detailsContent.summary.content.map(node => ({
+                type: node.type,
+                tagName: node.tagName,
+                value: node.type === 'text' ? node.value : undefined,
+                childCount: node.children?.length,
+                properties: node.properties
+              }))
+            },
+            content: {
+              nodeCount: detailsContent.content.length,
+              nodes: detailsContent.content.map(node => ({
+                type: node.type,
+                tagName: node.tagName,
+                value: node.type === 'text' ? node.value : undefined,
+                childCount: node.children?.length,
+                properties: node.properties
+              }))
+            }
+          })
+
           return createDetailsElement(detailsContent, parent, index)
         }
 

From 0ee8584266a8cde4c7db412fddbefabd3ca644dd Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Tue, 17 Dec 2024 23:48:23 -0500
Subject: [PATCH 20/23] Working implementation w/ useful logs to test

---
 lib/rehype-sn.js | 2 +-
 test-cases.md    | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)
 create mode 100644 test-cases.md

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 52d8d1e9d..5e59d5197 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -615,4 +615,4 @@ function createDetailsElement(detailsContent, parent, index) {
   )
 
   return [SKIP, detailsContent.endIndex]
-}
\ No newline at end of file
+}
diff --git a/test-cases.md b/test-cases.md
new file mode 100644
index 000000000..0519ecba6
--- /dev/null
+++ b/test-cases.md
@@ -0,0 +1 @@
+ 
\ No newline at end of file

From c96a4d24efbc0de477b4149ccda258d44412313b Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Wed, 18 Dec 2024 00:43:11 -0500
Subject: [PATCH 21/23] Cleanup and reorg new code to bottom

---
 lib/rehype-sn.js | 492 +++++++++++++++++++----------------------------
 1 file changed, 198 insertions(+), 294 deletions(-)

diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 5e59d5197..43290478d 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -13,57 +13,212 @@ const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)',
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
 
-// Helper to safely stringify node content
-function safeStringify(obj, depth = 0) {
-  if (depth > 2) return '[Nested Object]' // Prevent infinite recursion
-  try {
-    return JSON.stringify(obj, (key, value) => {
-      if (key === 'parent') return '[Parent]' // Skip circular parent refs
-      if (typeof value === 'object' && value !== null && depth < 2) {
-        return Object.fromEntries(
-          Object.entries(value).map(([k, v]) => [k, safeStringify(v, depth + 1)])
-        )
-      }
-      return value
-    }, 2)
-  } catch (e) {
-    return String(obj)
-  }
-}
-
-// Helper to print node info
-function logNode(prefix, node, detailed = false) {
-  const nodeInfo = {
-    type: node.type,
-    tagName: node.tagName,
-    childCount: node.children?.length,
-    value: node.type === 'text' || node.type === 'raw' 
-      ? node.value.substring(0, 100) + (node.value.length > 100 ? '...' : '')
-      : undefined,
-    properties: node.properties
-  }
-  
-  console.log(`${prefix} Node:`, safeStringify(nodeInfo))
-  
-  if (detailed && node.children?.length > 0) {
-    console.log(`${prefix} Children:`)
-    node.children.forEach((child, i) => {
-      logNode(`${prefix}   [${i}]`, child)
-    })
-  }
-}
-
 export default function rehypeSN(options = {}) {
   const { stylers = [] } = options
 
   return function transformer(tree) {
     try {
       visit(tree, (node, index, parent) => {
+
+        // Leave all other existing handlers unchanged
+        if (node.tagName === 'code') {
+          node.properties.inline = !(parent && parent.tagName === 'pre')
+        }
+
+        // handle headings
+        if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
+          const nodeText = toString(node)
+          const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
+          node.properties.id = headingId
+          // Create a new link element
+          const linkElement = {
+            type: 'element',
+            tagName: 'headlink',
+            properties: {
+              href: `#${headingId}`
+            },
+            children: [{ type: 'text', value: nodeText }]
+          }
+          // Replace the heading's children with the new link element
+          node.children = [linkElement]
+          return [SKIP]
+        }
+
+        // if img is wrapped in a link, remove the link
+        if (node.tagName === 'a' && node.children.length === 1 && node.children[0].tagName === 'img') {
+          parent.children[index] = node.children[0]
+          return index
+        }
+
+        // handle internal links
+        if (node.tagName === 'a') {
+          try {
+            if (node.properties.href.includes('#itemfn-')) {
+              node.tagName = 'footnote'
+            } else {
+              const { itemId, linkText } = parseInternalLinks(node.properties.href)
+              if (itemId) {
+                node.tagName = 'item'
+                node.properties.id = itemId
+                if (node.properties.href === toString(node)) {
+                  node.children[0].value = linkText
+                }
+              }
+            }
+          } catch {
+            // ignore errors like invalid URLs
+          }
+        }
+
+        // only show a link as an embed if it doesn't have text siblings
+        if (node.tagName === 'a' &&
+            !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
+            toString(node) === node.properties.href) {
+          const embed = parseEmbedUrl(node.properties.href)
+          if (embed) {
+            node.tagName = 'embed'
+            node.properties = { ...embed, src: node.properties.href }
+          } else {
+            node.tagName = 'autolink'
+          }
+        }
+
+        // if the link text is a URL, just show the URL
+        if (node.tagName === 'a' && isMisleadingLink(toString(node), node.properties.href)) {
+          node.children = [{ type: 'text', value: node.properties.href }]
+          return [SKIP]
+        }
+
+        // Handle @mentions and ~subs
+        if (node.type === 'text') {
+          const newChildren = []
+          let lastIndex = 0
+          let match
+          let childrenConsumed = 1
+          let text = toString(node)
+
+          const combinedRegex = new RegExp(mentionRegex.source + '|' + subRegex.source, 'gi')
+
+          // handle @__username__ or ~__sub__
+          if (['@', '~'].includes(node.value) &&
+              parent.children[index + 1]?.tagName === 'strong' &&
+              parent.children[index + 1].children[0]?.type === 'text') {
+            childrenConsumed = 2
+            text = node.value + '__' + toString(parent.children[index + 1]) + '__'
+          }
+
+          while ((match = combinedRegex.exec(text)) !== null) {
+            if (lastIndex < match.index) {
+              newChildren.push({ type: 'text', value: text.slice(lastIndex, match.index) })
+            }
+
+            const [fullMatch, mentionMatch, subMatch] = match
+            const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch) : replaceSub(fullMatch, subMatch)
+
+            if (replacement) {
+              newChildren.push(replacement)
+            } else {
+              newChildren.push({ type: 'text', value: fullMatch })
+            }
+
+            lastIndex = combinedRegex.lastIndex
+          }
+
+          if (newChildren.length > 0) {
+            if (lastIndex < text.length) {
+              newChildren.push({ type: 'text', value: text.slice(lastIndex) })
+            }
+            parent.children.splice(index, childrenConsumed, ...newChildren)
+            return index + newChildren.length
+          }
+        }
+
+        // Handle Nostr IDs
+        if (node.type === 'text') {
+          const newChildren = []
+          let lastIndex = 0
+          let match
+
+          while ((match = nostrIdRegex.exec(node.value)) !== null) {
+            if (lastIndex < match.index) {
+              newChildren.push({ type: 'text', value: node.value.slice(lastIndex, match.index) })
+            }
+
+            newChildren.push(replaceNostrId(match[0], match[0]))
+            lastIndex = nostrIdRegex.lastIndex
+          }
+
+          if (lastIndex < node.value.length) {
+            newChildren.push({ type: 'text', value: node.value.slice(lastIndex) })
+          }
+
+          if (newChildren.length > 0) {
+            parent.children.splice(index, 1, ...newChildren)
+            return index + newChildren.length
+          }
+        }
+
+        // handle custom tags
+        if (node.type === 'element') {
+          // Existing stylers handling
+          for (const { startTag, endTag, className } of stylers) {
+            for (let i = 0; i < node.children.length - 2; i++) {
+              const [start, text, end] = node.children.slice(i, i + 3)
+
+              if (start?.type === 'raw' && start?.value === startTag &&
+                  text?.type === 'text' &&
+                  end?.type === 'raw' && end?.value === endTag) {
+                const newChild = {
+                  type: 'element',
+                  tagName: 'span',
+                  properties: { className: [className] },
+                  children: [{ type: 'text', value: text.value }]
+                }
+                node.children.splice(i, 3, newChild)
+              }
+            }
+          }
+        }
+
+        // merge adjacent images and empty paragraphs into a single image collage
+        if ((node.tagName === 'img' || isImageOnlyParagraph(node)) && Array.isArray(parent.children)) {
+          const adjacentNodes = [node]
+          let nextIndex = index + 1
+          const siblings = parent.children
+          const somethingBefore = parent.children[index - 1] && parent.children[index - 1].tagName !== 'p'
+          let somethingAfter = false
+
+          while (nextIndex < siblings.length) {
+            const nextNode = siblings[nextIndex]
+            if (!nextNode) break
+            if (nextNode.tagName === 'img' || isImageOnlyParagraph(nextNode)) {
+              adjacentNodes.push(nextNode)
+              nextIndex++
+            } else if (nextNode.type === 'text' && typeof nextNode.value === 'string' && !nextNode.value.trim()) {
+              nextIndex++
+            } else {
+              somethingAfter = true
+              break
+            }
+          }
+
+          if (adjacentNodes.length > 0) {
+            const allImages = adjacentNodes.flatMap(n =>
+              n.tagName === 'img' ? [n] : (Array.isArray(n.children) ? n.children.filter(child => child.tagName === 'img') : [])
+            )
+            const collageNode = {
+              type: 'element',
+              tagName: 'p',
+              children: allImages,
+              properties: { onlyImages: true, somethingBefore, somethingAfter }
+            }
+            parent.children.splice(index, nextIndex - index, collageNode)
+            return index + 1
+          }
+        }
+
         // Handle details/summary tags
         if (node.type === 'raw' && node.value.includes('<details>')) {
-          console.log('\nšŸ” Found details tag at index:', index)
-          logNode('šŸ“Œ Details', node, true)
-          
           const detailsContent = {
             summary: {
               content: [],
@@ -77,16 +232,11 @@ export default function rehypeSN(options = {}) {
 
           // Handle self-contained details block
           if (node.value.includes('</details>')) {
-            console.log('šŸ“ Found self-contained details block')
             let content = node.value
             
             // Extract summary if present
             const summaryMatch = content.match(/<summary>(.*?)<\/summary>/s)
             if (summaryMatch) {
-              console.log('šŸ“Œ Found summary in self-contained block:', {
-                fullMatch: summaryMatch[0],
-                content: summaryMatch[1].trim()
-              })
               detailsContent.summary.content.push({
                 type: 'text',
                 value: summaryMatch[1].trim()
@@ -102,7 +252,6 @@ export default function rehypeSN(options = {}) {
               .trim()
             
             if (cleanedContent) {
-              console.log('šŸ“ Keeping content from self-contained block:', cleanedContent)
               detailsContent.content.push({
                 type: 'text',
                 value: cleanedContent
@@ -118,10 +267,6 @@ export default function rehypeSN(options = {}) {
           // Check for summary in opening node
           const summaryMatch = cleanedContent.match(/<summary>(.*?)<\/summary>/s)
           if (summaryMatch) {
-            console.log('\nšŸ“Œ Found summary in opening details node:', {
-              fullMatch: summaryMatch[0],
-              content: summaryMatch[1].trim()
-            })
             detailsContent.summary.content.push({
               type: 'text',
               value: summaryMatch[1].trim()
@@ -131,7 +276,6 @@ export default function rehypeSN(options = {}) {
           }
           
           if (cleanedContent.trim()) {
-            console.log('šŸ“ Keeping content from opening tag node:', cleanedContent)
             detailsContent.content.push({
               type: 'text',
               value: cleanedContent.trim()
@@ -139,7 +283,6 @@ export default function rehypeSN(options = {}) {
           }
 
           // Collect remaining content
-          console.log('\nšŸ“ Starting content collection...')
           let currentIndex = index
           let foundClosing = false
           
@@ -147,23 +290,15 @@ export default function rehypeSN(options = {}) {
             const currentNode = parent.children[++currentIndex]
             if (!currentNode) break
             
-            console.log(`\nšŸ‘€ Examining node at index ${currentIndex}:`)
-            logNode('   ', currentNode)
-            
             // Handle summary tags if we haven't found a complete summary yet
             if (!detailsContent.summary.complete) {
               if (currentNode.type === 'raw' && currentNode.value.includes('<summary>')) {
-                console.log('\nšŸ“Œ Found summary tag in node:', {
-                  type: currentNode.type,
-                  value: currentNode.value
-                })
                 detailsContent.summary.found = true
                 const summaryMatch = currentNode.value.match(/<summary>(.*?)<\/summary>/s)
                 if (summaryMatch) {
                   // Keep any text that appears before the summary tag
                   const beforeSummary = currentNode.value.substring(0, currentNode.value.indexOf('<summary>')).trim()
                   if (beforeSummary) {
-                    console.log('šŸ“ Keeping content before summary tag:', beforeSummary)
                     detailsContent.content.push({
                       type: 'text',
                       value: beforeSummary
@@ -171,7 +306,6 @@ export default function rehypeSN(options = {}) {
                   }
 
                   // Complete summary found in one node
-                  console.log('šŸ“„ Extracted summary content:', summaryMatch[1].trim())
                   detailsContent.summary.content.push({
                     type: 'text',
                     value: summaryMatch[1].trim()
@@ -184,7 +318,6 @@ export default function rehypeSN(options = {}) {
                   ).trim()
                   
                   if (afterSummary) {
-                    console.log('šŸ“ Keeping content after summary tag:', afterSummary)
                     detailsContent.content.push({
                       type: 'text',
                       value: afterSummary
@@ -195,7 +328,6 @@ export default function rehypeSN(options = {}) {
                 // If no match, it means the summary continues in next nodes
                 const afterOpen = currentNode.value.replace(/<summary>/g, '').trim()
                 if (afterOpen) {
-                  console.log('šŸ“ Found partial summary content:', afterOpen)
                   detailsContent.summary.content.push({
                     type: 'text',
                     value: afterOpen
@@ -209,7 +341,6 @@ export default function rehypeSN(options = {}) {
                 if (currentNode.type === 'raw' && currentNode.value.includes('</summary>')) {
                   const beforeClose = currentNode.value.replace(/<\/summary>/g, '').trim()
                   if (beforeClose) {
-                    console.log('šŸ“ Found closing summary content:', beforeClose)
                     detailsContent.summary.content.push({
                       type: 'text',
                       value: beforeClose
@@ -220,10 +351,6 @@ export default function rehypeSN(options = {}) {
                 }
                 // Add to summary content
                 if (currentNode.type === 'text' || currentNode.type === 'element') {
-                  console.log('šŸ“ Adding summary node:', {
-                    type: currentNode.type,
-                    content: currentNode.type === 'text' ? currentNode.value : '[element]'
-                  })
                   detailsContent.summary.content.push(currentNode)
                   continue
                 }
@@ -274,7 +401,6 @@ export default function rehypeSN(options = {}) {
                 }
               }
 
-              console.log('āœ… Found closing details tag')
               detailsContent.endIndex = currentIndex
               foundClosing = true
               break
@@ -287,234 +413,12 @@ export default function rehypeSN(options = {}) {
           }
 
           if (!foundClosing) {
-            console.log('āš ļø No closing tag found, skipping...')
             return SKIP
           }
 
-          // Add comprehensive logging of collected content
-          console.log('\nšŸ“¦ Final collected content:', {
-            summary: {
-              complete: detailsContent.summary.complete,
-              nodeCount: detailsContent.summary.content.length,
-              nodes: detailsContent.summary.content.map(node => ({
-                type: node.type,
-                tagName: node.tagName,
-                value: node.type === 'text' ? node.value : undefined,
-                childCount: node.children?.length,
-                properties: node.properties
-              }))
-            },
-            content: {
-              nodeCount: detailsContent.content.length,
-              nodes: detailsContent.content.map(node => ({
-                type: node.type,
-                tagName: node.tagName,
-                value: node.type === 'text' ? node.value : undefined,
-                childCount: node.children?.length,
-                properties: node.properties
-              }))
-            }
-          })
-
           return createDetailsElement(detailsContent, parent, index)
         }
 
-        // Leave all other existing handlers unchanged
-        if (node.tagName === 'code') {
-          node.properties.inline = !(parent && parent.tagName === 'pre')
-        }
-
-        // handle headings
-        if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
-          const nodeText = toString(node)
-          const headingId = slug(nodeText.replace(/[^\w\-\s]+/gi, ''))
-          node.properties.id = headingId
-          // Create a new link element
-          const linkElement = {
-            type: 'element',
-            tagName: 'headlink',
-            properties: {
-              href: `#${headingId}`
-            },
-            children: [{ type: 'text', value: nodeText }]
-          }
-          // Replace the heading's children with the new link element
-          node.children = [linkElement]
-          return [SKIP]
-        }
-
-        // if img is wrapped in a link, remove the link
-        if (node.tagName === 'a' && node.children.length === 1 && node.children[0].tagName === 'img') {
-          parent.children[index] = node.children[0]
-          return index
-        }
-
-        // handle internal links
-        if (node.tagName === 'a') {
-          try {
-            if (node.properties.href.includes('#itemfn-')) {
-              node.tagName = 'footnote'
-            } else {
-              const { itemId, linkText } = parseInternalLinks(node.properties.href)
-              if (itemId) {
-                node.tagName = 'item'
-                node.properties.id = itemId
-                if (node.properties.href === toString(node)) {
-                  node.children[0].value = linkText
-                }
-              }
-            }
-          } catch {
-            // ignore errors like invalid URLs
-          }
-        }
-
-        // only show a link as an embed if it doesn't have text siblings
-        if (node.tagName === 'a' &&
-            !parent.children.some(s => s.type === 'text' && s.value.trim()) &&
-            toString(node) === node.properties.href) {
-          const embed = parseEmbedUrl(node.properties.href)
-          if (embed) {
-            node.tagName = 'embed'
-            node.properties = { ...embed, src: node.properties.href }
-          } else {
-            node.tagName = 'autolink'
-          }
-        }
-
-        // if the link text is a URL, just show the URL
-        if (node.tagName === 'a' && isMisleadingLink(toString(node), node.properties.href)) {
-          node.children = [{ type: 'text', value: node.properties.href }]
-          return [SKIP]
-        }
-
-        // Handle @mentions and ~subs
-        if (node.type === 'text') {
-          const newChildren = []
-          let lastIndex = 0
-          let match
-          let childrenConsumed = 1
-          let text = toString(node)
-
-          const combinedRegex = new RegExp(mentionRegex.source + '|' + subRegex.source, 'gi')
-
-          // handle @__username__ or ~__sub__
-          if (['@', '~'].includes(node.value) &&
-              parent.children[index + 1]?.tagName === 'strong' &&
-              parent.children[index + 1].children[0]?.type === 'text') {
-            childrenConsumed = 2
-            text = node.value + '__' + toString(parent.children[index + 1]) + '__'
-          }
-
-          while ((match = combinedRegex.exec(text)) !== null) {
-            if (lastIndex < match.index) {
-              newChildren.push({ type: 'text', value: text.slice(lastIndex, match.index) })
-            }
-
-            const [fullMatch, mentionMatch, subMatch] = match
-            const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch) : replaceSub(fullMatch, subMatch)
-
-            if (replacement) {
-              newChildren.push(replacement)
-            } else {
-              newChildren.push({ type: 'text', value: fullMatch })
-            }
-
-            lastIndex = combinedRegex.lastIndex
-          }
-
-          if (newChildren.length > 0) {
-            if (lastIndex < text.length) {
-              newChildren.push({ type: 'text', value: text.slice(lastIndex) })
-            }
-            parent.children.splice(index, childrenConsumed, ...newChildren)
-            return index + newChildren.length
-          }
-        }
-
-        // Handle Nostr IDs
-        if (node.type === 'text') {
-          const newChildren = []
-          let lastIndex = 0
-          let match
-
-          while ((match = nostrIdRegex.exec(node.value)) !== null) {
-            if (lastIndex < match.index) {
-              newChildren.push({ type: 'text', value: node.value.slice(lastIndex, match.index) })
-            }
-
-            newChildren.push(replaceNostrId(match[0], match[0]))
-            lastIndex = nostrIdRegex.lastIndex
-          }
-
-          if (lastIndex < node.value.length) {
-            newChildren.push({ type: 'text', value: node.value.slice(lastIndex) })
-          }
-
-          if (newChildren.length > 0) {
-            parent.children.splice(index, 1, ...newChildren)
-            return index + newChildren.length
-          }
-        }
-
-        // handle custom tags
-        if (node.type === 'element') {
-          // Existing stylers handling
-          for (const { startTag, endTag, className } of stylers) {
-            for (let i = 0; i < node.children.length - 2; i++) {
-              const [start, text, end] = node.children.slice(i, i + 3)
-
-              if (start?.type === 'raw' && start?.value === startTag &&
-                  text?.type === 'text' &&
-                  end?.type === 'raw' && end?.value === endTag) {
-                const newChild = {
-                  type: 'element',
-                  tagName: 'span',
-                  properties: { className: [className] },
-                  children: [{ type: 'text', value: text.value }]
-                }
-                node.children.splice(i, 3, newChild)
-              }
-            }
-          }
-        }
-
-        // merge adjacent images and empty paragraphs into a single image collage
-        if ((node.tagName === 'img' || isImageOnlyParagraph(node)) && Array.isArray(parent.children)) {
-          const adjacentNodes = [node]
-          let nextIndex = index + 1
-          const siblings = parent.children
-          const somethingBefore = parent.children[index - 1] && parent.children[index - 1].tagName !== 'p'
-          let somethingAfter = false
-
-          while (nextIndex < siblings.length) {
-            const nextNode = siblings[nextIndex]
-            if (!nextNode) break
-            if (nextNode.tagName === 'img' || isImageOnlyParagraph(nextNode)) {
-              adjacentNodes.push(nextNode)
-              nextIndex++
-            } else if (nextNode.type === 'text' && typeof nextNode.value === 'string' && !nextNode.value.trim()) {
-              nextIndex++
-            } else {
-              somethingAfter = true
-              break
-            }
-          }
-
-          if (adjacentNodes.length > 0) {
-            const allImages = adjacentNodes.flatMap(n =>
-              n.tagName === 'img' ? [n] : (Array.isArray(n.children) ? n.children.filter(child => child.tagName === 'img') : [])
-            )
-            const collageNode = {
-              type: 'element',
-              tagName: 'p',
-              children: allImages,
-              properties: { onlyImages: true, somethingBefore, somethingAfter }
-            }
-            parent.children.splice(index, nextIndex - index, collageNode)
-            return index + 1
-          }
-        }
       })
 
       return tree

From 41e362e1e5f5f130955175d97b4fa25a83f61803 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Wed, 18 Dec 2024 01:18:26 -0500
Subject: [PATCH 22/23] linting

---
 components/text.js         |  4 +--
 components/text.module.css | 39 +++++++++++++++++++++++++++++
 lib/rehype-sn.js           | 50 +++++++++++++++-----------------------
 3 files changed, 61 insertions(+), 32 deletions(-)

diff --git a/components/text.js b/components/text.js
index 36b06cf0e..757a568e5 100644
--- a/components/text.js
+++ b/components/text.js
@@ -255,7 +255,7 @@ function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...pr
   )
 }
 
-function Details({ children, node, ...props }) {
+function Details ({ children, node, ...props }) {
   return (
     <details className={styles.details} {...props}>
       {children}
@@ -263,7 +263,7 @@ function Details({ children, node, ...props }) {
   )
 }
 
-function Summary({ children, node, ...props }) {
+function Summary ({ children, node, ...props }) {
   return (
     <summary className={styles.summary} {...props}>
       {children}
diff --git a/components/text.module.css b/components/text.module.css
index b526da1ac..7b5dc2fd8 100644
--- a/components/text.module.css
+++ b/components/text.module.css
@@ -439,3 +439,42 @@
 }
 
 /* Details/Summary styling */
+.details {
+    border-left: 2px solid var(--theme-quoteBar);
+    padding-left: 0.75rem;
+    margin: calc(var(--grid-gap) * 0.5) 0;
+    transition: border-color 0.2s ease;
+}
+
+.details[open] {
+    border-left-color: #f7931a; 
+}
+
+.summary {
+    cursor: pointer;
+    user-select: none;
+    color: #8b949e; 
+    transition: color 0.2s ease;
+    padding: 0.25rem 0;
+}
+
+.summary:hover {
+    color: #ffd700; 
+}
+
+
+.summary::marker,
+.summary::-webkit-details-marker {
+    color: #8b949e;
+}
+
+.details[open] > .summary::marker,
+.details[open] > .summary::-webkit-details-marker {
+    color: #f7931a;
+}
+
+
+.details > *:not(.summary) {
+    margin-top: calc(var(--grid-gap) * 0.5);
+    padding-bottom: calc(var(--grid-gap) * 0.25);
+}
diff --git a/lib/rehype-sn.js b/lib/rehype-sn.js
index 43290478d..6ee4241ab 100644
--- a/lib/rehype-sn.js
+++ b/lib/rehype-sn.js
@@ -13,14 +13,13 @@ const mentionRegex = new RegExp('@(' + userGroup + '(?:\\/' + userGroup + ')?)',
 const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
 const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g
 
-export default function rehypeSN(options = {}) {
+export default function rehypeSN (options = {}) {
   const { stylers = [] } = options
 
-  return function transformer(tree) {
+  return function transformer (tree) {
     try {
       visit(tree, (node, index, parent) => {
-
-        // Leave all other existing handlers unchanged
+        // Handle inline code property
         if (node.tagName === 'code') {
           node.properties.inline = !(parent && parent.tagName === 'pre')
         }
@@ -233,7 +232,7 @@ export default function rehypeSN(options = {}) {
           // Handle self-contained details block
           if (node.value.includes('</details>')) {
             let content = node.value
-            
+
             // Extract summary if present
             const summaryMatch = content.match(/<summary>(.*?)<\/summary>/s)
             if (summaryMatch) {
@@ -244,13 +243,13 @@ export default function rehypeSN(options = {}) {
               detailsContent.summary.complete = true
               content = content.replace(/<summary>.*?<\/summary>/s, '')
             }
-            
+
             // Clean remaining content
             const cleanedContent = content
               .replace(/<details>/g, '')
               .replace(/<\/details>/g, '')
               .trim()
-            
+
             if (cleanedContent) {
               detailsContent.content.push({
                 type: 'text',
@@ -263,7 +262,7 @@ export default function rehypeSN(options = {}) {
 
           // Clean opening details tag and handle potential summary
           let cleanedContent = node.value.replace(/<details>/g, '')
-          
+
           // Check for summary in opening node
           const summaryMatch = cleanedContent.match(/<summary>(.*?)<\/summary>/s)
           if (summaryMatch) {
@@ -274,7 +273,7 @@ export default function rehypeSN(options = {}) {
             detailsContent.summary.complete = true
             cleanedContent = cleanedContent.replace(/<summary>.*?<\/summary>/s, '')
           }
-          
+
           if (cleanedContent.trim()) {
             detailsContent.content.push({
               type: 'text',
@@ -285,11 +284,11 @@ export default function rehypeSN(options = {}) {
           // Collect remaining content
           let currentIndex = index
           let foundClosing = false
-          
+
           while (currentIndex < parent.children.length) {
             const currentNode = parent.children[++currentIndex]
             if (!currentNode) break
-            
+
             // Handle summary tags if we haven't found a complete summary yet
             if (!detailsContent.summary.complete) {
               if (currentNode.type === 'raw' && currentNode.value.includes('<summary>')) {
@@ -316,7 +315,7 @@ export default function rehypeSN(options = {}) {
                   const afterSummary = currentNode.value.substring(
                     currentNode.value.indexOf('</summary>') + '</summary>'.length
                   ).trim()
-                  
+
                   if (afterSummary) {
                     detailsContent.content.push({
                       type: 'text',
@@ -335,7 +334,7 @@ export default function rehypeSN(options = {}) {
                 }
                 continue
               }
-              
+
               // If we're collecting summary content
               if (detailsContent.summary.found) {
                 if (currentNode.type === 'raw' && currentNode.value.includes('</summary>')) {
@@ -360,9 +359,8 @@ export default function rehypeSN(options = {}) {
             // Check for closing details tag
             const hasClosingTag = (currentNode.type === 'raw' && currentNode.value.includes('</details>')) ||
                                 (currentNode.type === 'element' && toString(currentNode).includes('</details>'))
-            
+
             if (hasClosingTag) {
-              let cleanedContent
               if (currentNode.type === 'raw') {
                 const textBeforeClosing = currentNode.value.substring(0, currentNode.value.indexOf('</details>'))
                 if (textBeforeClosing.includes('\n')) {
@@ -418,18 +416,17 @@ export default function rehypeSN(options = {}) {
 
           return createDetailsElement(detailsContent, parent, index)
         }
-
       })
 
       return tree
     } catch (error) {
-      console.error('āŒ Error in rehypeSN transformer:', error)
+      console.error('Error in rehypeSN transformer:', error)
       return tree
     }
   }
 }
 
-function isImageOnlyParagraph(node) {
+function isImageOnlyParagraph (node) {
   return node &&
     node.tagName === 'p' &&
     Array.isArray(node.children) &&
@@ -439,7 +436,7 @@ function isImageOnlyParagraph(node) {
     )
 }
 
-function replaceMention(value, username) {
+function replaceMention (value, username) {
   return {
     type: 'element',
     tagName: 'mention',
@@ -448,7 +445,7 @@ function replaceMention(value, username) {
   }
 }
 
-function replaceSub(value, sub) {
+function replaceSub (value, sub) {
   return {
     type: 'element',
     tagName: 'sub',
@@ -457,7 +454,7 @@ function replaceSub(value, sub) {
   }
 }
 
-function replaceNostrId(value, id) {
+function replaceNostrId (value, id) {
   return {
     type: 'element',
     tagName: 'a',
@@ -466,7 +463,7 @@ function replaceNostrId(value, id) {
   }
 }
 
-function isMisleadingLink(text, href) {
+function isMisleadingLink (text, href) {
   let misleading = false
 
   if (/^\s*(\w+\.)+\w+/.test(text)) {
@@ -482,12 +479,7 @@ function isMisleadingLink(text, href) {
 }
 
 // Helper to create details element
-function createDetailsElement(detailsContent, parent, index) {
-  console.log('\nšŸ”Ø Creating details element with:', {
-    hasSummary: detailsContent.summary.complete,
-    contentNodes: detailsContent.content.length
-  })
-
+function createDetailsElement (detailsContent, parent, index) {
   const detailsElement = {
     type: 'element',
     tagName: 'details',
@@ -504,12 +496,10 @@ function createDetailsElement(detailsContent, parent, index) {
       children: detailsContent.summary.content
     }
     detailsElement.children.push(summaryElement)
-    console.log('✨ Added summary element')
   }
 
   // Add main content
   detailsElement.children.push(...detailsContent.content)
-  console.log('✨ Added content elements')
 
   // Replace nodes
   parent.children.splice(

From f166ffd72ebe258c4418e7610625e4de3f980848 Mon Sep 17 00:00:00 2001
From: krav <kravhen@gmail.com>
Date: Fri, 20 Dec 2024 00:08:55 -0500
Subject: [PATCH 23/23] styling touchup and cleanup

---
 components/text.module.css | 34 ++++++++++------------------------
 1 file changed, 10 insertions(+), 24 deletions(-)

diff --git a/components/text.module.css b/components/text.module.css
index 7b5dc2fd8..5ad30e23a 100644
--- a/components/text.module.css
+++ b/components/text.module.css
@@ -440,41 +440,27 @@
 
 /* Details/Summary styling */
 .details {
-    border-left: 2px solid var(--theme-quoteBar);
-    padding-left: 0.75rem;
+    border: 1px solid rgba(220, 220, 220, 0.5);
+    border-radius: 4px;
+    padding: 1rem;
     margin: calc(var(--grid-gap) * 0.5) 0;
-    transition: border-color 0.2s ease;
+    transition: all 0.2s ease;
 }
 
 .details[open] {
-    border-left-color: #f7931a; 
+    border-color: rgba(249, 217, 94, 0.5);
 }
 
 .summary {
     cursor: pointer;
-    user-select: none;
-    color: #8b949e; 
+    color: rgba(220, 220, 220, 0.5);
     transition: color 0.2s ease;
-    padding: 0.25rem 0;
 }
 
-.summary:hover {
-    color: #ffd700; 
-}
-
-
-.summary::marker,
-.summary::-webkit-details-marker {
-    color: #8b949e;
+.details[open] > .summary {
+    color: rgba(249, 217, 94, 0.5);
 }
 
-.details[open] > .summary::marker,
-.details[open] > .summary::-webkit-details-marker {
-    color: #f7931a;
-}
-
-
-.details > *:not(.summary) {
-    margin-top: calc(var(--grid-gap) * 0.5);
-    padding-bottom: calc(var(--grid-gap) * 0.25);
+.summary:hover {
+    color: #f9d95e;
 }