rename to client
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
# Markdown Support in msg.html
|
||||
|
||||
## Current State
|
||||
|
||||
### How messages render today
|
||||
In [`msg.html`](../www/msg.html:779), the [`renderThread()`](../www/msg.html:756) function renders message content using **`textContent`**:
|
||||
|
||||
```js
|
||||
const content = document.createElement('div');
|
||||
content.textContent = msg.content || '';
|
||||
```
|
||||
|
||||
This means all message text is displayed as plain text — no links, no bold, no formatting of any kind. Even URLs are not clickable.
|
||||
|
||||
### Existing assets in the project
|
||||
1. **[`marked.min.js`](../www/js/marked.min.js:1)** — marked v9.0.3 is already bundled in the project. It is used by [`note.html`](../www/note.html:257) for rendering long-form note content as markdown into an iframe.
|
||||
2. **[`htmlFormatText()`](../www/js/utilities.mjs:515)** — A simple utility that converts URLs to `<a>` links and image URLs to `<img>` tags. Used by [`post-interactions.mjs`](../www/js/post-interactions.mjs:300) for social feed posts.
|
||||
|
||||
---
|
||||
|
||||
## Industry Standard for Messaging Formatting
|
||||
|
||||
### What major messaging platforms do
|
||||
|
||||
| Platform | Formatting Approach |
|
||||
|----------|-------------------|
|
||||
| **Slack** | Custom markdown subset: `*bold*`, `_italic_`, `~strikethrough~`, `` `code` ``, ` ```code blocks``` `, `> blockquote`, links, emoji shortcodes |
|
||||
| **Discord** | Full markdown subset: `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``, ` ```lang code blocks``` `, `> blockquote`, `\|\| spoiler \|\|`, links |
|
||||
| **Telegram** | Markdown + HTML: `**bold**`, `__italic__`, `` `code` ``, ` ```code blocks``` `, `~~strikethrough~~`, `\|\| spoiler \|\|`, links |
|
||||
| **WhatsApp** | Limited markdown: `*bold*`, `_italic_`, `~strikethrough~`, `` `code` `` |
|
||||
| **Signal** | Limited markdown: `*bold*`, `_italic_`, `~~strikethrough~~`, `||spoiler||` |
|
||||
| **Matrix/Element** | Full markdown rendering with HTML sanitization |
|
||||
| **iMessage** | No markdown; rich text via attributed strings |
|
||||
|
||||
### Nostr ecosystem conventions
|
||||
- **NIP-01** kind 1 notes and kind 14 DMs use **plain text content** — no official markdown NIP exists
|
||||
- In practice, most Nostr clients render:
|
||||
- URLs as clickable links
|
||||
- Image URLs as inline images
|
||||
- `nostr:` URIs as profile/event links
|
||||
- Some clients like **Amethyst**, **Damus**, and **Coracle** render basic markdown in notes
|
||||
- **NIP-23** long-form content (kind 30023) explicitly uses markdown
|
||||
|
||||
### Recommended approach for Nostr DMs
|
||||
The industry consensus for chat/messaging is a **sanitized markdown subset** — not full markdown. The key principles:
|
||||
|
||||
1. **Render inline formatting**: bold, italic, strikethrough, inline code
|
||||
2. **Render code blocks**: fenced code blocks with syntax highlighting optional
|
||||
3. **Auto-link URLs**: make URLs clickable
|
||||
4. **Render image URLs**: show inline image previews
|
||||
5. **Blockquotes**: useful for quoting previous messages
|
||||
6. **Sanitize HTML**: prevent XSS — never allow raw HTML tags
|
||||
7. **Do NOT render**: headings, tables, horizontal rules, or complex block elements that don't make sense in chat bubbles
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Raw message content] --> B{Contains markdown?}
|
||||
B -->|Any content| C[DOMPurify sanitize raw text]
|
||||
C --> D[marked.parse with chat-safe options]
|
||||
D --> E[DOMPurify sanitize HTML output]
|
||||
E --> F[Set innerHTML on bubble]
|
||||
|
||||
G[Reply input box] --> H[Get plaintext from contenteditable]
|
||||
H --> I[Send as plain text over NIP-04/NIP-44/NIP-17]
|
||||
|
||||
style A fill:#1a1a2e,stroke:#e94560,color:#eee
|
||||
style F fill:#1a1a2e,stroke:#16c79a,color:#eee
|
||||
style I fill:#1a1a2e,stroke:#16c79a,color:#eee
|
||||
```
|
||||
|
||||
### Steps
|
||||
|
||||
#### 1. Add marked.min.js script tag to msg.html
|
||||
Include the existing [`marked.min.js`](../www/js/marked.min.js:1) in the `<head>` of [`msg.html`](../www/msg.html), just like [`note.html`](../www/note.html:257) does:
|
||||
|
||||
```html
|
||||
<script src="./js/marked.min.js"></script>
|
||||
```
|
||||
|
||||
#### 2. Add DOMPurify for XSS protection
|
||||
Since messages come from untrusted third parties, HTML sanitization is critical. Add DOMPurify via CDN:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
|
||||
```
|
||||
|
||||
Alternatively, download and bundle it locally like `marked.min.js`.
|
||||
|
||||
#### 3. Configure marked for chat-safe rendering
|
||||
Create a chat-specific marked configuration that disables features inappropriate for chat bubbles:
|
||||
|
||||
```js
|
||||
function configureMarkedForChat() {
|
||||
marked.setOptions({
|
||||
breaks: true, // Convert \n to <br> - essential for chat
|
||||
gfm: true, // GitHub Flavored Markdown - strikethrough, tables
|
||||
pedantic: false,
|
||||
silent: true // Don't throw on bad markdown
|
||||
});
|
||||
|
||||
// Custom renderer to limit what gets rendered
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
// Disable headings in chat - render as bold text instead
|
||||
renderer.heading = function(text, level) {
|
||||
return `<strong>${text}</strong><br>`;
|
||||
};
|
||||
|
||||
// Disable horizontal rules
|
||||
renderer.hr = function() {
|
||||
return '<br>';
|
||||
};
|
||||
|
||||
// Make links open in new tab
|
||||
renderer.link = function(href, title, text) {
|
||||
const titleAttr = title ? ` title="${title}"` : '';
|
||||
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
};
|
||||
|
||||
// Render images inline with max-width constraint
|
||||
renderer.image = function(href, title, text) {
|
||||
const titleAttr = title ? ` title="${title}"` : '';
|
||||
return `<img src="${href}" alt="${text}"${titleAttr} style="max-width:100%;border-radius:6px;">`;
|
||||
};
|
||||
|
||||
marked.use({ renderer });
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Create a renderMessageContent helper function
|
||||
Add a function that safely converts message text to formatted HTML:
|
||||
|
||||
```js
|
||||
function renderMessageContent(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// Parse markdown to HTML
|
||||
let html = '';
|
||||
try {
|
||||
html = marked.parse(text);
|
||||
} catch (e) {
|
||||
// Fallback to escaped plain text
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Sanitize to prevent XSS
|
||||
if (window.DOMPurify) {
|
||||
html = DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [
|
||||
'b', 'i', 'em', 'strong', 'a', 'code', 'pre',
|
||||
'br', 'p', 'ul', 'ol', 'li', 'blockquote',
|
||||
'del', 's', 'img', 'span'
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
'href', 'target', 'rel', 'title', 'alt', 'src', 'style'
|
||||
],
|
||||
ALLOW_DATA_ATTR: false
|
||||
});
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Update renderThread to use innerHTML with sanitized markdown
|
||||
Change the content rendering in [`renderThread()`](../www/msg.html:756) from `textContent` to `innerHTML`:
|
||||
|
||||
**Before** (line 779):
|
||||
```js
|
||||
content.textContent = msg.content || '';
|
||||
```
|
||||
|
||||
**After**:
|
||||
```js
|
||||
content.innerHTML = renderMessageContent(msg.content || '');
|
||||
```
|
||||
|
||||
#### 6. Add CSS styles for markdown inside chat bubbles
|
||||
Add styles scoped to `.msgBubble` so markdown elements look appropriate in chat context:
|
||||
|
||||
```css
|
||||
.msgBubble p {
|
||||
margin: 0 0 0.3em 0;
|
||||
}
|
||||
.msgBubble p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.msgBubble code {
|
||||
background: rgba(128,128,128,0.2);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 85%;
|
||||
}
|
||||
.msgBubble pre {
|
||||
background: rgba(0,0,0,0.15);
|
||||
padding: 6px 8px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.msgBubble pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.msgBubble blockquote {
|
||||
border-left: 3px solid var(--muted-color);
|
||||
margin: 4px 0;
|
||||
padding: 2px 8px;
|
||||
color: var(--muted-color);
|
||||
}
|
||||
.msgBubble a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.msgBubble img {
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.msgBubble ul, .msgBubble ol {
|
||||
margin: 4px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.msgBubble del, .msgBubble s {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Adjust white-space on msgBubble
|
||||
The current [`msgBubble`](../www/msg.html:178) CSS has `white-space: pre-wrap` which will conflict with markdown-rendered HTML. Change it:
|
||||
|
||||
**Before**:
|
||||
```css
|
||||
.msgBubble {
|
||||
white-space: pre-wrap;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**After**:
|
||||
```css
|
||||
.msgBubble {
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The `breaks: true` option in marked handles newline conversion to `<br>`, so `pre-wrap` is no longer needed.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **DOMPurify is essential** — messages come from untrusted Nostr users. Without sanitization, a malicious user could inject `<script>` tags or event handlers via crafted markdown.
|
||||
- The allowlist approach in DOMPurify config above is restrictive by design — only safe formatting tags are permitted.
|
||||
- `target="_blank"` links must include `rel="noopener noreferrer"` to prevent tab-nabbing.
|
||||
- Image rendering from arbitrary URLs has privacy implications — loading an image reveals the user's IP to the image host. Consider adding a click-to-load option for images in a future iteration.
|
||||
|
||||
## Summary of Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`www/msg.html`](../www/msg.html) | Add `marked.min.js` + DOMPurify script tags, add `configureMarkedForChat()` + `renderMessageContent()` functions, update `renderThread()` to use `innerHTML`, add markdown CSS styles, adjust `white-space` on `.msgBubble` |
|
||||
|
||||
## Optional Future Enhancements
|
||||
|
||||
- **Markdown toolbar** in the reply input box with buttons for bold, italic, code, link
|
||||
- **Markdown preview** toggle in the reply box showing formatted output before sending
|
||||
- **Syntax highlighting** for code blocks using highlight.js or Prism
|
||||
- **Click-to-load images** for privacy protection
|
||||
- **nostr: URI handling** to render `nostr:npub...` and `nostr:note...` as clickable profile/event links
|
||||
- **Emoji shortcode** support
|
||||
Reference in New Issue
Block a user